import type { Message } from "#cli/core/model";
import type { DimcodeUserToolsV1, McpServerSettings } from "#cli/compat/config";
import type {
  CustomProviderId,
  ProviderAdapter,
  ProviderCustomModel,
  ProviderState,
} from "#cli/compat/providers";
import type { CliAgentBridge as DimSdkAgent } from "../agent-bridge";
import type { DimSdkStore } from "../core/store";
import type { DimSdkSessionsApi } from "../keys";
import type { DimcodeUserCacheV1, DimcodeUserConfigV1 } from "./user-cache";
import { registerOAuthRuntimeSyncTarget } from "#cli/core/oauth-runtime-sync";
import { snapshotToUiMessages } from "#cli/core/session-ui-messages";
import { deriveSessionLastMessagePreviewFromMessages } from "#cli/core/sessions";
import {
  isErrorAssistantMessage,
  isInterruptedAssistantMessage,
} from "#cli/core/store/acp-transform";
import {
  isCompactTranscriptMessageId,
  isCompressionTranscriptMessageId,
} from "#cli/core/supplemental-transcript";
import { normalizeShortcutBindings } from "#cli/core/ui-settings";
import {
  buildUserToolsFromStore,
  hydrateToolsStateFromConfig,
  migrateLegacyCustomProvider,
  saveDimcodeUserTools,
  saveMcpJson,
} from "#cli/compat/config";
import {
  buildProviderSelectionCandidates,
  listBuiltinProviders,
  normalizeProviderId,
  resolveProviderSelection,
  sanitizeWorkspaceProviderSelections,
  stripUnknownActiveModelSelections,
} from "#cli/compat/providers";
import { watch } from "vue";
import { cloneLastRequestBudget, normalizeLastRequestBudget } from "../core/store/request-budget";
import {
  hydrateStoreFromSnapshot,
  snapshotToListItem,
  stripSupplementalTranscriptMessages,
  uiMessagesToSnapshotMessages,
} from "./persistence/snapshot";
import { buildUserCacheFromStore, buildUserConfigFromStore } from "./persistence/user-cache";
import { normalizeStr, saveDimcodeUserCache, saveDimcodeUserConfig } from "./user-cache";
import { createDebouncedAsyncTask } from "./utils/debounce";
import { normalizeLimit } from "./utils/normalize";

export type DimSdkSessionsBackend = Readonly<{
  list: () => Promise<unknown[]>;
  get: (id: string) => Promise<unknown | null>;
  create: (opts?: { cwd?: string }) => Promise<unknown>;
  destroy: (id: string) => Promise<void>;
  rename?: (id: string, title: string) => Promise<unknown | null>;
  createWithMessages?: (messages: Record<string, unknown>[]) => Promise<unknown>;
  updateMetadata?: (id: string, metadata: Record<string, unknown>) => Promise<unknown | null>;
}>;

type AgentSessionInvalidator = Readonly<{
  invalidateSession?: (sessionId: string) => void;
}>;

interface ToolsRefState {
  websearch?: {
    apiKey?: string;
    apiEndpoint?: string;
    numResults?: number;
  };
  mcpServers?: unknown;
  [key: string]: unknown;
}

interface ToolsRef {
  current: ToolsRefState;
}

const INTERRUPTED_PERSISTED_MESSAGE_LIMIT = 80;
const COMPACT_PERSISTED_MESSAGE_LIMIT = 80;
const COMPRESSION_PERSISTED_MESSAGE_LIMIT = 80;
const ERROR_PERSISTED_MESSAGE_LIMIT = 80;

type PersistedProviderConnection = NonNullable<
  NonNullable<DimcodeUserConfigV1["settings"]>["providerConnections"]
>[string];

type PersistedCustomProvider = NonNullable<
  NonNullable<DimcodeUserConfigV1["settings"]>["customProviders"]
>[number];

type PersistedActiveModelSelections = NonNullable<
  NonNullable<DimcodeUserConfigV1["settings"]>["activeModelByProvider"]
>;

type PersistedWorkspaceSelection = NonNullable<
  NonNullable<DimcodeUserCacheV1["settings"]>["workspaceSelectionByCwd"]
>[string];

type PersistedWorkspaceSelections = NonNullable<
  NonNullable<DimcodeUserCacheV1["settings"]>["workspaceSelectionByCwd"]
>;

function asRecord(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  return value as Record<string, unknown>;
}

function normalizePersistedReasoning(value: unknown): ProviderCustomModel["reasoning"] | undefined {
  const reasoning = asRecord(value);
  if (reasoning.supported !== true) return undefined;
  return typeof reasoning.default === "boolean"
    ? { supported: true, default: reasoning.default }
    : { supported: true };
}

function normalizePersistedProviderCustomModel(value: unknown): ProviderCustomModel | null {
  const model = asRecord(value);
  const id = normalizeStr(model.id);
  if (!id) return null;

  const name = normalizeStr(model.name);
  const limit = normalizeLimit(model.limit);
  const vision = typeof model.vision === "boolean" ? model.vision : undefined;
  const reasoning = normalizePersistedReasoning(model.reasoning);

  return {
    id,
    ...(name ? { name } : {}),
    ...(limit ? { limit } : {}),
    ...(typeof vision === "boolean" ? { vision } : {}),
    ...(reasoning ? { reasoning } : {}),
  };
}

function normalizePersistedProviderConnection(
  value: PersistedProviderConnection | unknown,
): ProviderState["connections"][string] | null {
  const connection = asRecord(value);
  const apiKey = normalizeStr(connection.apiKey);
  const baseUrl = normalizeStr(connection.baseUrl);
  const models = (Array.isArray(connection.models) ? connection.models : [])
    .map(normalizePersistedProviderCustomModel)
    .filter((model): model is ProviderCustomModel => Boolean(model));

  if (!apiKey && !baseUrl && models.length === 0) return null;

  return {
    ...(apiKey ? { apiKey } : {}),
    ...(baseUrl ? { baseUrl } : {}),
    ...(models.length ? { models } : {}),
  };
}

function normalizePersistedActiveModelByProvider(
  value: PersistedActiveModelSelections | undefined,
): ProviderState["activeModelByProvider"] {
  const entries = Object.entries(asRecord(value))
    .map(([providerId, modelId]) => {
      const nextProviderId = normalizeStr(providerId);
      const nextModelId = normalizeStr(modelId);
      if (!nextProviderId || !nextModelId) return null;
      return [nextProviderId, nextModelId] as const;
    })
    .filter((entry): entry is readonly [string, string] => Boolean(entry));
  return Object.fromEntries(entries);
}

function normalizePersistedProviderAdapter(value: unknown): ProviderAdapter | undefined {
  const adapter = normalizeStr(value);
  if (
    adapter === "openai" ||
    adapter === "openai-responses" ||
    adapter === "gemini" ||
    adapter === "anthropic"
  ) {
    return adapter;
  }
  return undefined;
}

function normalizePersistedCustomProviderId(value: unknown): CustomProviderId | undefined {
  const id = normalizeStr(value);
  if (id?.startsWith("cp_")) return id as CustomProviderId;
  return undefined;
}

function normalizePersistedCustomProvider(
  value: PersistedCustomProvider | unknown,
): ProviderState["customProviders"][number] | null {
  const provider = asRecord(value);
  const id = normalizePersistedCustomProviderId(provider.id);
  const name = normalizeStr(provider.name);
  const adapter = normalizePersistedProviderAdapter(provider.adapter);
  if (!id || !name || !adapter) return null;

  const baseUrl = normalizeStr(provider.baseUrl);
  const apiKey = normalizeStr(provider.apiKey);
  const models = (Array.isArray(provider.models) ? provider.models : [])
    .map(normalizePersistedProviderCustomModel)
    .filter((model): model is ProviderCustomModel => Boolean(model));

  return {
    id,
    name,
    adapter,
    ...(baseUrl ? { baseUrl } : {}),
    ...(apiKey ? { apiKey } : {}),
    models,
  };
}

function normalizePersistedWorkspaceSelection(
  value: PersistedWorkspaceSelection | unknown,
): { providerId: string; modelId?: string } | null {
  const selection = asRecord(value);
  const providerId = normalizeStr(selection.providerId);
  if (!providerId) return null;
  const modelId = normalizeStr(selection.modelId);
  return {
    providerId,
    ...(modelId ? { modelId } : {}),
  };
}

function normalizePersistedWorkspaceSelections(
  value: PersistedWorkspaceSelections | undefined,
): Record<string, { providerId: string; modelId?: string }> {
  const entries = Object.entries(asRecord(value))
    .map(([cwd, selection]) => {
      const key = normalizeStr(cwd);
      const nextSelection = normalizePersistedWorkspaceSelection(selection);
      if (!key || !nextSelection) return null;
      return [key, nextSelection] as const;
    })
    .filter((entry): entry is readonly [string, { providerId: string; modelId?: string }] =>
      Boolean(entry),
    );
  return Object.fromEntries(entries);
}

function cloneMcpServers(
  servers: readonly McpServerSettings[] | null | undefined,
): readonly McpServerSettings[] | undefined {
  if (!servers || servers.length === 0) return undefined;
  return clonePersistableValue(servers);
}

function assistantMarkdownOnlySignature(msg: Extract<Message, { role: "assistant" }>): string {
  return msg.parts
    .filter((p): p is Extract<typeof p, { type: "markdown" }> => p.type === "markdown")
    .map((p) => String(p.markdown ?? ""))
    .join("\n")
    .trim();
}

function assistantPersistenceSignature(msg: Extract<Message, { role: "assistant" }>): string {
  const status = msg.parts.find(
    (p): p is Extract<typeof p, { type: "status" }> => p.type === "status",
  );
  const statusText = String(status?.text ?? "").trim();
  const markdown = assistantMarkdownOnlySignature(msg);
  const toolCalls = msg.parts
    .filter((p): p is Extract<typeof p, { type: "tool_call" }> => p.type === "tool_call")
    .map((p) => {
      const id = String(p.call.id ?? "").trim();
      const name = String(p.call.name ?? "").trim();
      return `${name}:${id}`;
    })
    .filter(Boolean)
    .join("|");
  return [statusText, markdown, toolCalls].filter(Boolean).join("\n").trim();
}

function userPersistenceSignature(msg: Extract<Message, { role: "user" }>): string {
  const content = String(msg.content ?? "").trim();
  const focusFiles = (msg.focusFiles ?? [])
    .map((file) => String(file ?? "").trim())
    .filter(Boolean)
    .join("\n");
  return [content, focusFiles].filter(Boolean).join("\n").trim();
}

function messagePersistenceSignature(message: Message): string {
  if (message.role === "assistant") {
    const sig = assistantPersistenceSignature(message);
    return sig ? `assistant:${sig}` : "";
  }
  if (message.role === "user") {
    const sig = userPersistenceSignature(message);
    return sig ? `user:${sig}` : "";
  }
  return "";
}

function snapshotId(snapshot: unknown): string {
  const obj = asRecord(snapshot);
  return String(obj.sessionId ?? obj.id ?? "").trim();
}

function collectAnchoredAssistantRoundsForPersistence(
  messages: readonly Message[],
  opts: Readonly<{
    limit: number;
    include: (message: Extract<Message, { role: "assistant" }>) => boolean;
  }>,
): Message[] {
  const rounds = new Map<string, Message[]>();
  let lastUser: Extract<Message, { role: "user" }> | null = null;
  for (let i = 0; i < messages.length; i++) {
    const current = messages[i];
    if (current?.role === "user") {
      lastUser = current;
      continue;
    }
    if (!current || current.role !== "assistant") continue;
    if (!opts.include(current)) continue;
    const round: Message[] = [];
    if (lastUser) round.push(lastUser);
    round.push(current);
    const assistantSig = messagePersistenceSignature(current);
    if (!assistantSig) continue;
    const anchorSig = lastUser ? messagePersistenceSignature(lastUser) : "";
    const key = `${anchorSig}\n---\n${assistantSig}`;
    rounds.delete(key);
    rounds.set(key, round);
  }
  return [...rounds.values()].slice(-opts.limit).flat().map(clonePersistableValue);
}

function collectInterruptedRoundMessagesForPersistence(messages: readonly Message[]): Message[] {
  return collectAnchoredAssistantRoundsForPersistence(messages, {
    limit: INTERRUPTED_PERSISTED_MESSAGE_LIMIT,
    include: isInterruptedAssistantMessage,
  });
}

function collectCompactMessagesForPersistence(messages: readonly Message[]): Message[] {
  const unique = new Map<string, Extract<Message, { role: "assistant" }>>();
  for (const m of messages) {
    if (!m || m.role !== "assistant") continue;
    const id = String(m.id ?? "").trim();
    if (!isCompactTranscriptMessageId(id)) continue;
    if (id.startsWith("compact-restored-") || id.startsWith("restored-compact-")) {
      continue;
    }
    const hasVisibleParts = m.parts.some((p) => {
      if (p.type === "markdown") return String(p.markdown ?? "").trim().length > 0;
      if (p.type === "info") return String(p.text ?? "").trim().length > 0;
      return false;
    });
    if (!hasVisibleParts) continue;
    const sig = messagePersistenceSignature(m);
    if (!sig) continue;
    unique.delete(sig);
    unique.set(sig, m);
  }
  const out = [...unique.values()];
  if (out.length <= COMPACT_PERSISTED_MESSAGE_LIMIT) return out;
  return out.slice(-COMPACT_PERSISTED_MESSAGE_LIMIT);
}

function collectCompressionMessagesForPersistence(messages: readonly Message[]): Message[] {
  const unique = new Map<string, Extract<Message, { role: "assistant" }>>();
  for (const m of messages) {
    if (!m || m.role !== "assistant") continue;
    const id = String(m.id ?? "").trim();
    if (!isCompressionTranscriptMessageId(id)) continue;
    if (id.startsWith("compression-restored-") || id.startsWith("restored-compression-")) {
      continue;
    }
    const hasVisibleParts = m.parts.some((p) => {
      if (p.type === "markdown") return String(p.markdown ?? "").trim().length > 0;
      if (p.type === "info") return String(p.text ?? "").trim().length > 0;
      return false;
    });
    if (!hasVisibleParts) continue;
    const sig = messagePersistenceSignature(m);
    if (!sig) continue;
    unique.delete(sig);
    unique.set(sig, m);
  }
  const out = [...unique.values()];
  if (out.length <= COMPRESSION_PERSISTED_MESSAGE_LIMIT) return out;
  return out.slice(-COMPRESSION_PERSISTED_MESSAGE_LIMIT);
}

function clonePersistableValue<T>(value: T): T {
  return JSON.parse(JSON.stringify(value)) as T;
}

function assistantMessageHasDurableTranscriptParts(message: Message): boolean {
  if (!message || message.role !== "assistant") return false;
  return message.parts.some((part) => {
    if (part.type === "markdown") return String(part.markdown ?? "").trim().length > 0;
    if (part.type === "thinking") return String(part.content ?? "").trim().length > 0;
    if (part.type === "tool_call") return true;
    if (part.type === "tool_result") return true;
    return false;
  });
}

function hasPendingTranscriptTail(messages: readonly Message[]): boolean {
  for (let index = messages.length - 1; index >= 0; index -= 1) {
    const message = messages[index];
    if (!message) continue;
    if (message.role === "tool") continue;
    if (message.role === "user") return true;
    if (message.role === "assistant") return !assistantMessageHasDurableTranscriptParts(message);
    return false;
  }
  return false;
}

function hydrateStorePreviewMessages(messages: readonly Record<string, unknown>[]): Message[] {
  return snapshotToUiMessages({ context: { messages } }) as Message[];
}

function collectErrorMessagesForPersistence(messages: readonly Message[]): Message[] {
  // Persist the user anchor together with the error assistant so hydration can
  // restore the error block to its original turn before falling back to timestamps.
  return collectAnchoredAssistantRoundsForPersistence(messages, {
    limit: ERROR_PERSISTED_MESSAGE_LIMIT,
    include: isErrorAssistantMessage,
  });
}

export function buildTranscriptPayload(messages: readonly Message[]): Readonly<{
  messages: Record<string, unknown>[];
  messageCount: number;
  lastMessagePreview?: string;
}> {
  const uiMessages = Array.isArray(messages) ? messages : [];
  const canonicalMessages = stripSupplementalTranscriptMessages(uiMessages);
  const snapshotMessages = uiMessagesToSnapshotMessages(canonicalMessages);
  const restoredMessages = hydrateStorePreviewMessages(snapshotMessages);
  const lastMessage = uiMessages.at(-1);
  const previewMessages = lastMessage?.role === "tool" ? restoredMessages : uiMessages;
  return {
    messages: snapshotMessages,
    messageCount: uiMessages.length,
    lastMessagePreview: deriveSessionLastMessagePreviewFromMessages(previewMessages) || undefined,
  };
}

/**
 * Session list + lightweight user settings persistence.
 *
 * Session storage is handled by DimCode's SessionManager/StateStore (see examples),
 * not by the CLI's `cache.json`.
 */
export function createDimSdkPersistence(
  opts: Readonly<{
    cacheFile: string;
    cache: DimcodeUserCacheV1;
    configFile: string;
    config: DimcodeUserConfigV1;
    toolsFile: string;
    tools: DimcodeUserToolsV1;
    mcpJsonFile?: string;
    mcpServers?: McpServerSettings[];
    sessionsBackend?: DimSdkSessionsBackend | null;
    agent?: AgentSessionInvalidator | DimSdkAgent;
    toolsRef?: ToolsRef;
    deferPersist?: boolean;
  }>,
): Readonly<{
  sessions: DimSdkSessionsApi | undefined;
  onStoreCreated: (store: DimSdkStore) => void;
  rehydrate: (
    next: Readonly<{
      cache?: DimcodeUserCacheV1;
      config?: DimcodeUserConfigV1;
      tools?: DimcodeUserToolsV1;
    }>,
  ) => void;
}> {
  let storeRef: DimSdkStore | null = null;
  let persistEnabled = !opts.deferPersist;
  let schedulePersist: (() => void) | null = null;

  const backend = opts.sessionsBackend ?? null;

  const sessions: DimSdkSessionsApi | undefined = backend
    ? {
        async list() {
          const list = await backend.list();
          const items = list
            .map(snapshotToListItem)
            .filter((item): item is NonNullable<ReturnType<typeof snapshotToListItem>> =>
              Boolean(item),
            );
          return items;
        },
        async get(id: string) {
          const sessionId = String(id ?? "").trim();
          if (!sessionId) return null;
          return backend.get(sessionId);
        },
        async reload(id: string) {
          const sessionId = String(id ?? "").trim();
          if (!sessionId || !storeRef) return false;
          if (String(storeRef.state.sessionId ?? "") !== sessionId) return false;
          if (storeRef.state.runningAgentRound) return false;
          if (storeRef.state.paused) return false;
          if (storeRef.state.approval?.active?.status === "pending") return false;

          const snapshot = await backend.get(sessionId);
          if (!snapshot) return false;

          hydrateStoreFromSnapshot(storeRef, snapshot, {
            preserveLocalSyntheticErrors: true,
            preserveLocalInterruptedMessages: true,
            preserveLocalCompactMessages: true,
            preserveLocalCompressionMessages: true,
          });
          const invalidator = opts.agent as AgentSessionInvalidator | undefined;
          if (invalidator?.invalidateSession) invalidator.invalidateSession(sessionId);
          return true;
        },
        async use(id: string) {
          const sessionId = String(id ?? "").trim();
          if (!sessionId || !storeRef) return;
          if (String(storeRef.state.sessionId ?? "") === sessionId) return;
          const snapshot = await backend.get(sessionId);
          if (!snapshot) return;
          hydrateStoreFromSnapshot(storeRef, snapshot);
          // Invalidate cached session in agent to force reload with correct middleware
          const invalidator = opts.agent as AgentSessionInvalidator | undefined;
          if (invalidator?.invalidateSession) {
            invalidator.invalidateSession(sessionId);
          }
        },
        async createNew() {
          if (!storeRef) return;
          const snapshot = await backend.create();
          if (!snapshot) return;
          const newSessionId = snapshotId(snapshot);
          hydrateStoreFromSnapshot(storeRef, snapshot);
          // Invalidate cached session in agent to force reload with correct middleware
          if (newSessionId) {
            const invalidator = opts.agent as AgentSessionInvalidator | undefined;
            if (invalidator?.invalidateSession) invalidator.invalidateSession(newSessionId);
          }
        },
        async delete(id: string) {
          const sessionId = String(id ?? "").trim();
          if (!sessionId) return;

          // Deletion should work even if the UI store isn't available yet (e.g. called before
          // `onStoreCreated`). Store hydration is optional; backend persistence is required.
          await backend.destroy(sessionId);
          if (storeRef && String(storeRef.state.sessionId ?? "") === sessionId) {
            // If the deleted session was the current one, clear the session ID.
            // A new session should only be created when the user actually enters chat
            // and sends a message, not automatically on deletion.
            storeRef.state.sessionId = "";
          }
        },
        async rename(id: string, title: string) {
          if (!storeRef) return;
          if (typeof backend.rename !== "function") return;
          const sessionId = String(id ?? "").trim();
          const nextTitle = String(title ?? "").trim();
          if (!sessionId || !nextTitle) return;
          const snapshot = await backend.rename(sessionId, nextTitle);
          if (snapshot && String(storeRef.state.sessionId ?? "") === sessionId) {
            hydrateStoreFromSnapshot(storeRef, snapshot, {
              preserveLocalSyntheticErrors: true,
              preserveLocalInterruptedMessages: true,
              preserveLocalCompactMessages: true,
              preserveLocalCompressionMessages: true,
            });
          }
        },
        async updateMetadata(id: string, metadata: Record<string, unknown>) {
          if (typeof backend.updateMetadata !== "function") return;
          const sessionId = String(id ?? "").trim();
          if (!sessionId) return;
          await backend.updateMetadata(sessionId, metadata);
        },
        async fork(opts2: Readonly<{ messages: readonly Message[] }>) {
          if (!storeRef) return;
          const seed = Array.isArray(opts2?.messages) ? opts2.messages : [];
          if (typeof backend.createWithMessages !== "function") {
            // Best-effort fallback: create empty session and let the UI keep the seed transcript.
            const snapshot = await backend.create();
            if (snapshot) storeRef.state.sessionId = snapshotId(snapshot);
            storeRef.replaceMessages(seed);
            storeRef.clearRevertedMessages();
            storeRef.state.selectedToolCallId = null;
            storeRef.state.usage.prompt_tokens = 0;
            storeRef.state.usage.completion_tokens = 0;
            storeRef.state.usage.total_tokens = 0;
            storeRef.state.lastRequestBudget = null;
            return;
          }

          const snapshot = await backend.createWithMessages(
            uiMessagesToSnapshotMessages(stripSupplementalTranscriptMessages(seed)),
          );
          if (!snapshot) return;
          hydrateStoreFromSnapshot(storeRef, snapshot);
        },
      }
    : undefined;

  const replaceObjectContents = (target: object, source: object) => {
    for (const key of Object.keys(target)) Reflect.deleteProperty(target, key);
    Object.assign(target, source);
  };

  const applyHydration = (
    store: DimSdkStore,
    cache: DimcodeUserCacheV1,
    config: DimcodeUserConfigV1,
    tools: DimcodeUserToolsV1,
  ): boolean => {
    let changed = false;
    migrateLegacyCustomProvider(config);
    const saved = config.settings ?? {};
    const providerId = normalizeProviderId(saved.providerId);

    const provider = store.state.provider;

    if (providerId) provider.activeId = providerId;

    const normalizedConnections = Object.fromEntries(
      Object.entries(saved.providerConnections ?? {})
        .map(([id, connection]) => {
          const providerId = normalizeStr(id);
          const nextConnection = normalizePersistedProviderConnection(connection);
          if (!providerId || !nextConnection) return null;
          return [providerId, nextConnection] as const;
        })
        .filter((entry): entry is readonly [string, ProviderState["connections"][string]] =>
          Boolean(entry),
        ),
    );
    provider.connections = normalizedConnections;

    provider.activeModelByProvider = normalizePersistedActiveModelByProvider(
      saved.activeModelByProvider,
    );

    const customProviders: ProviderState["customProviders"] = Array.isArray(saved.customProviders)
      ? saved.customProviders
          .map(normalizePersistedCustomProvider)
          .filter((p): p is ProviderState["customProviders"][number] => Boolean(p))
      : [];
    provider.customProviders = customProviders;

    const activeProviderId = provider.activeId;
    if (activeProviderId && store.state.config.model) {
      const currentModel = String(store.state.config.model ?? "").trim();
      if (currentModel && !provider.activeModelByProvider[activeProviderId])
        provider.activeModelByProvider[activeProviderId] = currentModel;
    }

    hydrateToolsStateFromConfig(tools, store.state, opts.mcpServers);

    const savedUi = cache.settings ?? {};
    const uiState = store.state.ui;
    const env = store.state.context.env ?? {};
    const toolApprovalsEnv = normalizeStr(env.DIMCODE_TOOL_APPROVALS);
    if (!toolApprovalsEnv && savedUi.toolApprovals) {
      const raw = String(savedUi.toolApprovals);
      const mapped =
        raw === "auto" || raw === "all"
          ? ("auto" as const)
          : raw === "strict" || raw === "manual"
            ? ("strict" as const)
            : ("normal" as const);
      store.state.approval.mode = mapped;
    }
    if (typeof savedUi.themeMode === "string" && savedUi.themeMode)
      store.setThemeMode(savedUi.themeMode);
    if (typeof savedUi.statusCompact === "boolean") uiState.statusCompact = savedUi.statusCompact;
    if (typeof savedUi.onboardingSeenVersion === "string" && savedUi.onboardingSeenVersion) {
      uiState.onboarding = {
        ...uiState.onboarding,
        lastSeenVersion: savedUi.onboardingSeenVersion,
      };
    }
    if (savedUi.shortcuts) uiState.shortcuts = normalizeShortcutBindings(savedUi.shortcuts);
    if (typeof savedUi.greetingLastSeenDate === "string") {
      uiState.greeting = {
        ...uiState.greeting,
        lastSeenDate: savedUi.greetingLastSeenDate,
      };
    }
    if (savedUi.providerFunnelCounts && typeof savedUi.providerFunnelCounts === "object") {
      const counts = Object.fromEntries(
        Object.entries(savedUi.providerFunnelCounts)
          .map(([step, count]) => [String(step), Number(count)] as const)
          .filter(([step, count]) => step && Number.isFinite(count) && count > 0),
      );
      uiState.providerFunnel = {
        ...uiState.providerFunnel,
        counts,
        events: Array.isArray(uiState.providerFunnel?.events) ? uiState.providerFunnel.events : [],
      };
    }
    if (savedUi.workspaceSelectionByCwd && typeof savedUi.workspaceSelectionByCwd === "object") {
      uiState.workspaceSelectionByCwd = normalizePersistedWorkspaceSelections(
        savedUi.workspaceSelectionByCwd,
      );
    }

    const { candidates, existingProviderIds } = buildProviderSelectionCandidates({
      builtinProviders: listBuiltinProviders(),
      connections: provider.connections,
      customProviders,
      legacyCustom: provider.custom,
      rememberedModels: {
        ...provider.activeModelByProvider,
      },
    });
    const rememberedModels = stripUnknownActiveModelSelections(
      {
        ...provider.activeModelByProvider,
      },
      existingProviderIds,
    );
    if (JSON.stringify(provider.activeModelByProvider ?? {}) !== JSON.stringify(rememberedModels)) {
      provider.activeModelByProvider = rememberedModels;
      changed = true;
    }

    const sanitizedWorkspaceSelections = sanitizeWorkspaceProviderSelections({
      workspaceSelections: uiState.workspaceSelectionByCwd,
      candidates,
      rememberedModels,
    });
    if (
      JSON.stringify(uiState.workspaceSelectionByCwd ?? {}) !==
      JSON.stringify(sanitizedWorkspaceSelections)
    ) {
      uiState.workspaceSelectionByCwd = sanitizedWorkspaceSelections;
      changed = true;
    }

    const selection = resolveProviderSelection({
      candidates,
      preferredProviderId: String(provider.activeId ?? "").trim(),
      preferredModelId: String(store.state.config.model ?? "").trim(),
      rememberedModels,
    });
    const nextProviderId = String(selection.providerId ?? "").trim();
    const nextModelId = String(selection.modelId ?? "").trim();
    if (String(provider.activeId ?? "").trim() !== nextProviderId) {
      provider.activeId = nextProviderId;
      changed = true;
    }
    if (String(store.state.config.model ?? "").trim() !== nextModelId) {
      store.state.config.model = nextModelId;
      changed = true;
    }

    if (backend) {
      const currentId = String(store.state.sessionId ?? "").trim();
      if (currentId) {
        void backend
          .get(currentId)
          .then((snapshot) => {
            if (snapshot && storeRef) {
              hydrateStoreFromSnapshot(storeRef, snapshot, {
                preserveLocalSyntheticErrors: true,
                preserveLocalInterruptedMessages: true,
                preserveLocalCompactMessages: true,
                preserveLocalCompressionMessages: true,
              });
            }
          })
          .catch(() => {});
      }
    }

    return changed;
  };

  const onStoreCreated = (store: DimSdkStore) => {
    storeRef = store;
    applyHydration(store, opts.cache, opts.config, opts.tools);
    registerOAuthRuntimeSyncTarget({
      getStore: () => storeRef,
      updatePersistenceSnapshots: (next) => {
        if (next.cache) replaceObjectContents(opts.cache, next.cache);
        if (next.config) replaceObjectContents(opts.config, next.config);
      },
      invalidateSession: (sessionId) => {
        const invalidator = opts.agent as AgentSessionInvalidator | undefined;
        if (invalidator?.invalidateSession) invalidator.invalidateSession(sessionId);
      },
    });

    // Debounced cache/config save (UI settings + provider config; session state is in StateStore).
    const persist = createDebouncedAsyncTask(async () => {
      try {
        const nextCache: DimcodeUserCacheV1 = buildUserCacheFromStore({
          prev: opts.cache,
          store,
        });
        const nextConfig: DimcodeUserConfigV1 = buildUserConfigFromStore({
          prev: opts.config,
          store,
        });
        const nextTools: DimcodeUserToolsV1 = buildUserToolsFromStore({
          prev: opts.tools,
          store,
        });
        const mcpServers = cloneMcpServers(store.state.mcp?.servers) ?? [];
        const saves: Promise<void>[] = [
          saveDimcodeUserCache(opts.cacheFile, nextCache),
          saveDimcodeUserConfig(opts.configFile, nextConfig),
          saveDimcodeUserTools(opts.toolsFile, nextTools),
        ];
        if (opts.mcpJsonFile) {
          saves.push(saveMcpJson(opts.mcpJsonFile, mcpServers));
        }
        await Promise.all(saves);
      } catch (e) {
        console.warn("[dimcode] Failed to persist cache:", e);
      }
    }, 300);

    schedulePersist = () => {
      if (!persistEnabled) return;
      persist.schedule();
    };

    const persistSupplementalTranscript = createDebouncedAsyncTask(async () => {
      if (!backend || typeof backend.updateMetadata !== "function") {
        return;
      }
      const sessionId = String(store.state.sessionId ?? "").trim();
      if (!sessionId) return;
      if (store.state.runningAgentRound) return;
      if (hasPendingTranscriptTail(store.state.messages)) return;
      const interrupted = collectInterruptedRoundMessagesForPersistence(store.state.messages);
      const interruptedMessages = interrupted.map(clonePersistableValue);
      const compact = collectCompactMessagesForPersistence(store.state.messages);
      const compactMessages = uiMessagesToSnapshotMessages(compact);
      const compression = collectCompressionMessagesForPersistence(store.state.messages);
      const compressionMessages = uiMessagesToSnapshotMessages(compression);
      const errorMessages = collectErrorMessagesForPersistence(store.state.messages);
      const lastRequestBudget = normalizeLastRequestBudget(store.state.lastRequestBudget);
      const usagePayload = {
        promptTokens: Math.max(0, Number(store.state.usage.prompt_tokens) || 0),
        completionTokens: Math.max(0, Number(store.state.usage.completion_tokens) || 0),
        totalTokens: Math.max(0, Number(store.state.usage.total_tokens) || 0),
      };
      try {
        const payload: Record<string, unknown> = {
          interruptedMessages,
          compactMessages,
          compressionMessages,
          errorMessages,
          usage: usagePayload,
          ...(lastRequestBudget
            ? { lastRequestBudget: cloneLastRequestBudget(lastRequestBudget) }
            : {}),
        };
        await backend.updateMetadata(sessionId, { ui: payload });
      } catch {
        // Best-effort transcript persistence only.
      }
    }, 0);

    const scheduleSupplementalPersist = () => {
      persistSupplementalTranscript.schedule();
    };

    watch(() => store.state.sessionId, schedulePersist);
    watch(() => store.state.config, schedulePersist, { deep: true });
    watch(() => store.state.context, schedulePersist, { deep: true });
    watch(() => store.state.approval.mode, schedulePersist);
    watch(() => store.state.ui?.locale, schedulePersist);
    watch(() => store.state.ui?.theme, schedulePersist, { deep: true });
    watch(() => store.state.ui?.statusCompact, schedulePersist);
    watch(() => store.state.ui?.onboarding, schedulePersist, { deep: true });
    watch(() => store.state.ui?.shortcuts, schedulePersist, { deep: true });
    watch(() => store.state.ui?.greeting, schedulePersist, { deep: true });
    watch(() => store.state.provider, schedulePersist, { deep: true });
    watch(() => store.state.tools, schedulePersist, { deep: true });
    watch(() => store.state.mcp, schedulePersist, { deep: true });
    watch(() => store.state.sessionId, scheduleSupplementalPersist);
    watch(() => store.state.messages.length, scheduleSupplementalPersist);
    watch(() => store.state.lastRequestBudget, scheduleSupplementalPersist, {
      deep: true,
    });
    watch(
      () => store.state.runningAgentRound,
      (running, prev) => {
        if (prev && !running) scheduleSupplementalPersist();
      },
    );

    // Sync toolsRef with store.state.tools for dynamic API key retrieval
    if (opts.toolsRef) {
      watch(
        () => store.state.tools,
        (newTools) => {
          if (newTools) {
            opts.toolsRef!.current = {
              ...opts.toolsRef!.current,
              websearch: {
                ...opts.toolsRef!.current?.websearch,
                apiKey: newTools.websearch?.apiKey ?? "",
                apiEndpoint: newTools.websearch?.apiEndpoint,
                numResults: newTools.websearch?.numResults,
              },
              mcpServers:
                cloneMcpServers(store.state.mcp?.servers) ?? opts.toolsRef!.current?.mcpServers,
            };
          }
        },
        { deep: true, immediate: true },
      );

      watch(
        () => store.state.mcp?.servers,
        (servers) => {
          opts.toolsRef!.current = {
            ...opts.toolsRef!.current,
            mcpServers: cloneMcpServers(servers) ?? opts.toolsRef!.current?.mcpServers,
          };
        },
        { deep: true, immediate: true },
      );
    }

    schedulePersist();
  };

  const rehydrate = (
    next: Readonly<{
      cache?: DimcodeUserCacheV1;
      config?: DimcodeUserConfigV1;
      tools?: DimcodeUserToolsV1;
    }>,
  ) => {
    if (next.cache) replaceObjectContents(opts.cache, next.cache);
    if (next.config) replaceObjectContents(opts.config, next.config);
    if (next.tools) replaceObjectContents(opts.tools, next.tools);
    if (storeRef) applyHydration(storeRef, opts.cache, opts.config, opts.tools);
    if (!persistEnabled) persistEnabled = true;
    schedulePersist?.();
  };

  return { sessions, onStoreCreated, rehydrate } as const;
}
