import { randomUUID } from 'node:crypto';
import { getPathToLeaf } from './path.js';
import { SessionTreeProviderError, type SessionTreeProvider } from './provider.js';
import { buildSessionTreeSnapshot } from './snapshot.js';
import type {
  AppendSessionTreeEntryInput,
  CreateSessionTreeInput,
  SessionTreeEntry,
  SessionTreeMessage,
  SessionTreeSession,
  SessionTreeSnapshot,
  UpdateSessionTreeEntryPatch,
} from './types.js';
import type { NetaClawAgentSessionEntity } from '../entity/agent_session.js';
import type { NetaClawAgentSessionEntryEntity } from '../entity/agent_session_entry.js';

type SessionIdGenerator = () => string;
type EntryIdGenerator = (existingIds?: { has(id: string): boolean }) => string;
type NowGenerator = () => string;

type SessionRow = Omit<
  Partial<NetaClawAgentSessionEntity>,
  | 'sessionId'
  | 'provider'
  | 'rootEntryId'
  | 'leafEntryId'
  | 'cwd'
  | 'sessionFile'
  | 'parentSessionId'
  | 'agentId'
  | 'userId'
  | 'title'
  | 'status'
  | 'metadata'
  | 'createTime'
  | 'updateTime'
> & {
  id?: number;
  sessionId: string;
  provider: string;
  rootEntryId: string | null;
  leafEntryId: string | null;
  cwd?: string | null;
  sessionFile?: string | null;
  parentSessionId?: string | null;
  agentId?: string | null;
  userId?: string | null;
  title?: string | null;
  status?: string | null;
  metadata?: Record<string, unknown> | null;
  createTime?: string | Date;
  updateTime?: string | Date;
};

type EntryRow = Omit<
  Partial<NetaClawAgentSessionEntryEntity>,
  | 'sessionId'
  | 'entryId'
  | 'parentEntryId'
  | 'timestamp'
  | 'type'
  | 'payload'
  | 'metadata'
  | 'createTime'
  | 'updateTime'
> & {
  id?: number;
  sessionId: string;
  entryId: string;
  parentEntryId: string | null;
  entrySeq?: number | null;
  timestamp: string;
  type: string;
  payload?: Record<string, unknown> | null;
  metadata?: Record<string, unknown> | null;
  createTime?: string | Date;
  updateTime?: string | Date;
};

export interface SessionTreeRepositoryLike {
  find(options?: {
    where?: Record<string, unknown>;
    order?: Record<string, 'ASC' | 'DESC' | 'asc' | 'desc'>;
  }): Promise<unknown[]>;
  findOne(options: { where: Record<string, unknown> }): Promise<unknown | null>;
  findOneBy(where: Record<string, unknown>): Promise<unknown | null>;
  save(entity: unknown): Promise<unknown>;
  delete(where: Record<string, unknown>): Promise<unknown>;
}

export interface MySqlSessionTreeProviderOptions {
  sessionRepo: SessionTreeRepositoryLike;
  entryRepo: SessionTreeRepositoryLike;
  sessionIdGenerator?: SessionIdGenerator;
  entryIdGenerator?: EntryIdGenerator;
  now?: NowGenerator;
}

type RootEntryLike =
  | Pick<EntryRow, 'entryId' | 'parentEntryId'>
  | Pick<SessionTreeEntry, 'id' | 'parentId'>;

export class MySqlSessionTreeProvider implements SessionTreeProvider {
  private readonly sessionRepo: SessionTreeRepositoryLike;
  private readonly entryRepo: SessionTreeRepositoryLike;
  private readonly sessionIdGenerator: SessionIdGenerator;
  private readonly entryIdGenerator: EntryIdGenerator;
  private readonly now: NowGenerator;

  constructor(options: MySqlSessionTreeProviderOptions) {
    this.sessionRepo = options.sessionRepo;
    this.entryRepo = options.entryRepo;
    this.sessionIdGenerator = options.sessionIdGenerator ?? createDefaultSessionId;
    this.entryIdGenerator = options.entryIdGenerator ?? createDefaultEntryId;
    this.now = options.now ?? (() => new Date().toISOString());
  }

  async createSession(input: CreateSessionTreeInput): Promise<SessionTreeSession> {
    const sessionId = input.sessionId ?? this.sessionIdGenerator();
    const createdAt = input.createdAt ?? this.now();
    const updatedAt = input.updatedAt ?? createdAt;
    assertJsonSerializable(input.metadata, `createSession metadata for session ${sessionId}`);

    const row: SessionRow = {
      sessionId,
      provider: 'mysql',
      rootEntryId: input.rootEntryId ?? null,
      leafEntryId: input.leafEntryId ?? null,
      cwd: toNullable(input.cwd),
      sessionFile: toNullable(input.sessionFile),
      parentSessionId: toNullable(input.parentSessionId),
      agentId: normalizeIdentityValue(input.agentId),
      userId: normalizeIdentityValue(input.userId),
      title: toNullable(input.title),
      status: input.status ?? 'active',
      metadata: buildSessionMetadata(input.metadata, input.agentId, input.userId),
      createTime: createdAt,
      updateTime: updatedAt,
    };

    const saved = await this.sessionRepo.save(row) as SessionRow;
    return cloneSession(mapSessionRowToSession(saved, []));
  }

  async getSession(sessionId: string): Promise<SessionTreeSession | null> {
    const row = await this.sessionRepo.findOneBy({ sessionId }) as SessionRow | null;
    if (!row) {
      return null;
    }

    const entries = await this.listEntryRows(sessionId);
    return cloneSession(mapSessionRowToSession(row, entries));
  }

  async updateSession(sessionId: string, patch: Partial<SessionTreeSession>): Promise<SessionTreeSession> {
    const existing = await this.requireSessionRow(sessionId);
    assertJsonSerializable(patch.metadata, `updateSession metadata for session ${sessionId}`);
    const existingIdentityMeta = getSessionIdentityMetadata(existing.metadata);

    const updated: SessionRow = {
      ...existing,
      cwd: patch.cwd === undefined ? existing.cwd ?? null : toNullable(patch.cwd),
      sessionFile: patch.sessionFile === undefined ? existing.sessionFile ?? null : toNullable(patch.sessionFile),
      parentSessionId: patch.parentSessionId === undefined ? existing.parentSessionId ?? null : toNullable(patch.parentSessionId),
      agentId: patch.agentId === undefined ? existing.agentId ?? null : normalizeIdentityValue(patch.agentId),
      userId: patch.userId === undefined ? existing.userId ?? null : normalizeIdentityValue(patch.userId),
      title: patch.title === undefined ? existing.title ?? null : toNullable(patch.title),
      status: patch.status === undefined ? existing.status ?? 'active' : patch.status,
      metadata: buildSessionMetadata(
        patch.metadata === undefined ? stripSessionIdentityMetadata(existing.metadata) : patch.metadata,
        patch.agentId === undefined ? restoreIdentityValue(existing.agentId, existingIdentityMeta.agentIdType) : patch.agentId,
        patch.userId === undefined ? restoreIdentityValue(existing.userId, existingIdentityMeta.userIdType) : patch.userId,
      ),
      rootEntryId: existing.rootEntryId ?? null,
      leafEntryId: Object.prototype.hasOwnProperty.call(patch, 'leafEntryId')
        ? patch.leafEntryId ?? null
        : existing.leafEntryId ?? null,
      updateTime: patch.updatedAt ?? (Object.keys(patch).length > 0 ? this.now() : existing.updateTime),
    };

    const saved = await this.sessionRepo.save(withExistingPrimaryKey(existing, updated)) as SessionRow;
    const entries = await this.listEntryRows(sessionId);
    return cloneSession(mapSessionRowToSession(saved, entries));
  }

  async deleteSession(sessionId: string): Promise<void> {
    await this.entryRepo.delete({ sessionId });
    await this.sessionRepo.delete({ sessionId });
  }

  async listEntries(sessionId: string): Promise<SessionTreeEntry[]> {
    await this.requireSessionRow(sessionId);
    const rows = await this.listEntryRows(sessionId);
    return rows.map(row => cloneEntry(mapEntryRowToEntry(row)));
  }

  async appendEntry(sessionId: string, input: AppendSessionTreeEntryInput): Promise<SessionTreeEntry> {
    const sessionRow = await this.requireSessionRow(sessionId);
    const existingEntries = await this.listEntries(sessionId);
    assertAppendParentConstraint(existingEntries, sessionId, input.parentId);

    const existingIds = new Set(existingEntries.map(entry => entry.id));
    const entry = cloneEntry({
      ...input,
      id: input.id ?? this.entryIdGenerator(existingIds),
      timestamp: input.timestamp ?? this.now(),
    } as SessionTreeEntry);

    assertJsonSerializable(entry, `appendEntry input for session ${sessionId}`);

    if (existingIds.has(entry.id)) {
      throw new SessionTreeProviderError(`Entry ${entry.id} already exists in session ${sessionId}`);
    }

    const now = this.now();
    const row = mapEntryToRow(sessionId, entry, now, nextEntrySeq(existingEntries));
    await this.entryRepo.save(row);

    await this.sessionRepo.save(withExistingPrimaryKey(sessionRow, {
      ...sessionRow,
      rootEntryId: resolveRootEntryIdFromEntries(existingEntries) ?? entry.id,
      leafEntryId: entry.id,
      updateTime: now,
    }));

    return cloneEntry(entry);
  }

  async appendMessage(sessionId: string, message: SessionTreeMessage): Promise<SessionTreeEntry> {
    const session = await this.requireSession(sessionId);
    return this.appendEntry(sessionId, {
      parentId: session.leafEntryId,
      type: 'message',
      message,
    });
  }

  async appendThinkingLevelChange(sessionId: string, thinkingLevel: string): Promise<SessionTreeEntry> {
    const session = await this.requireSession(sessionId);
    return this.appendEntry(sessionId, {
      parentId: session.leafEntryId,
      type: 'thinking_level_change',
      thinkingLevel,
    });
  }

  async appendModelChange(sessionId: string, provider: string, modelId: string): Promise<SessionTreeEntry> {
    const session = await this.requireSession(sessionId);
    return this.appendEntry(sessionId, {
      parentId: session.leafEntryId,
      type: 'model_change',
      provider,
      modelId,
    });
  }

  async appendCompaction(sessionId: string, input: {
    summary: string;
    firstKeptEntryId: string;
    tokensBefore: number;
    details?: Record<string, unknown>;
  }): Promise<SessionTreeEntry> {
    const session = await this.requireSession(sessionId);
    return this.appendEntry(sessionId, {
      parentId: session.leafEntryId,
      type: 'compaction',
      summary: input.summary,
      firstKeptEntryId: input.firstKeptEntryId,
      tokensBefore: input.tokensBefore,
      details: input.details,
    });
  }

  async appendBranchSummary(sessionId: string, input: {
    fromId: string;
    summary: string;
    details?: Record<string, unknown>;
  }): Promise<SessionTreeEntry> {
    const session = await this.requireSession(sessionId);
    return this.appendEntry(sessionId, {
      parentId: session.leafEntryId,
      type: 'branch_summary',
      fromId: input.fromId,
      summary: input.summary,
      details: input.details,
    });
  }

  async appendLabelChange(sessionId: string, targetId: string, label: string | undefined): Promise<SessionTreeEntry> {
    const session = await this.requireSession(sessionId);
    return this.appendEntry(sessionId, {
      parentId: session.leafEntryId,
      type: 'label',
      targetId,
      label,
    });
  }

  async appendSessionInfo(sessionId: string, name?: string): Promise<SessionTreeEntry> {
    const session = await this.requireSession(sessionId);
    return this.appendEntry(sessionId, {
      parentId: session.leafEntryId,
      type: 'session_info',
      name,
    });
  }

  async updateEntry(sessionId: string, id: string, patch: UpdateSessionTreeEntryPatch): Promise<SessionTreeEntry> {
    const sessionRow = await this.requireSessionRow(sessionId);
    const row = await this.entryRepo.findOneBy({
      sessionId,
      entryId: id,
    }) as EntryRow | null;

    if (!row) {
      throw new SessionTreeProviderError(`Entry ${id} not found in session ${sessionId}`);
    }

    const current = mapEntryRowToEntry(row);
    assertImmutableEntryPatch(current, patch as Record<string, unknown>);
    assertJsonSerializable(patch, `updateEntry patch for entry ${id} in session ${sessionId}`);

    const updatedEntry = cloneEntry({
      ...current,
      ...cloneJsonValue(patch),
      id: current.id,
      type: current.type,
      parentId: current.parentId,
      timestamp: current.timestamp,
    } as SessionTreeEntry);

    const now = this.now();
    await this.entryRepo.save(withExistingPrimaryKey(row, {
      ...row,
      ...mapEntryToRow(sessionId, updatedEntry, now, row.entrySeq ?? 0),
      createTime: row.createTime,
      updateTime: now,
    }));
    await this.sessionRepo.save(withExistingPrimaryKey(sessionRow, {
      ...sessionRow,
      updateTime: now,
    }));

    return cloneEntry(updatedEntry);
  }

  async switchLeaf(sessionId: string, leafEntryId: string | null): Promise<SessionTreeSession> {
    const sessionRow = await this.requireSessionRow(sessionId);

    if (leafEntryId !== null) {
      const row = await this.entryRepo.findOneBy({
        sessionId,
        entryId: leafEntryId,
      }) as EntryRow | null;

      if (!row) {
        throw new SessionTreeProviderError(`Leaf entry ${leafEntryId} not found in session ${sessionId}`);
      }
    }

    const saved = await this.sessionRepo.save(withExistingPrimaryKey(sessionRow, {
      ...sessionRow,
      leafEntryId,
      updateTime: this.now(),
    })) as SessionRow;

    const entries = await this.listEntryRows(sessionId);
    return cloneSession(mapSessionRowToSession(saved, entries));
  }

  async resetLeaf(sessionId: string): Promise<SessionTreeSession> {
    return this.switchLeaf(sessionId, null);
  }

  async createBranchedSession(
    sessionId: string,
    leafEntryId: string,
    input: CreateSessionTreeInput = {},
  ): Promise<SessionTreeSession> {
    const { session, entries } = await this.requireNormalizedSessionState(sessionId);
    const pathEntries = getPathToLeaf(entries, leafEntryId);
    const created = await this.createSession({
      ...input,
      provider: 'mysql',
      parentSessionId: input.parentSessionId ?? sessionId,
      cwd: input.cwd ?? session.cwd,
      agentId: input.agentId ?? session.agentId,
      userId: input.userId ?? session.userId,
      title: input.title ?? session.title,
      metadata: input.metadata ?? session.metadata,
    });

    for (const entry of pathEntries) {
      await this.appendEntry(created.sessionId, cloneEntry(entry) as AppendSessionTreeEntryInput);
    }

    await this.switchLeaf(created.sessionId, leafEntryId);
    return (await this.getSession(created.sessionId))!;
  }

  async getActivePath(sessionId: string): Promise<SessionTreeEntry[]> {
    const { session, entries } = await this.requireNormalizedSessionState(sessionId);

    if (session.leafEntryId === null) {
      return [];
    }

    return getPathToLeaf(entries, session.leafEntryId).map(cloneEntry);
  }

  async getSnapshot(sessionId: string): Promise<SessionTreeSnapshot> {
    const { session, entries } = await this.requireNormalizedSessionState(sessionId);
    return cloneSnapshot(buildSessionTreeSnapshot(session, entries));
  }

  private async requireSession(sessionId: string): Promise<SessionTreeSession> {
    const { session } = await this.requireNormalizedSessionState(sessionId);
    return session;
  }

  private async requireSessionRow(sessionId: string): Promise<SessionRow> {
    const row = await this.sessionRepo.findOneBy({ sessionId }) as SessionRow | null;
    if (!row) {
      throw new SessionTreeProviderError(`Session ${sessionId} not found`);
    }
    return row;
  }

  private async listEntryRows(sessionId: string): Promise<EntryRow[]> {
    return await this.entryRepo.find({
      where: { sessionId },
      order: {
        timestamp: 'ASC',
        entrySeq: 'ASC',
        entryId: 'ASC',
      },
    }) as EntryRow[];
  }

  private async requireNormalizedSessionState(sessionId: string): Promise<{
    session: SessionTreeSession;
    entries: SessionTreeEntry[];
  }> {
    const row = await this.requireSessionRow(sessionId);
    const entryRows = await this.listEntryRows(sessionId);
    const entries = entryRows.map(mapEntryRowToEntry);

    return {
      session: mapSessionRowToSession(row, entryRows),
      entries,
    };
  }
}

function mapSessionRowToSession(row: SessionRow, entries: EntryRow[]): SessionTreeSession {
  const identityMeta = getSessionIdentityMetadata(row.metadata);
  const rootEntryId = resolveRootEntryId(entries);
  const leafEntryId = resolveLeafEntryId(row, entries);
  return {
    sessionId: row.sessionId,
    provider: 'mysql',
    rootEntryId,
    leafEntryId,
    cwd: toUndefined(row.cwd),
    sessionFile: toUndefined(row.sessionFile),
    parentSessionId: toUndefined(row.parentSessionId),
    agentId: restoreIdentityValue(row.agentId, identityMeta.agentIdType),
    userId: restoreIdentityValue(row.userId, identityMeta.userIdType),
    title: toUndefined(row.title),
    status: row.status ?? 'active',
    metadata: stripSessionIdentityMetadata(row.metadata),
    createdAt: normalizeTimestamp(row.createTime),
    updatedAt: normalizeTimestamp(row.updateTime) ?? normalizeTimestamp(row.createTime),
  };
}

function resolveRootEntryId(entries: RootEntryLike[]): string | null {
  const root = entries.find(entry => ('parentEntryId' in entry ? entry.parentEntryId : entry.parentId) === null);
  if (!root) {
    return null;
  }

  return 'entryId' in root ? root.entryId : root.id;
}

function resolveRootEntryIdFromEntries(
  entries: Array<Pick<SessionTreeEntry, 'id' | 'parentId'>>,
): string | null {
  return entries.find(entry => entry.parentId === null)?.id ?? null;
}

function resolveLeafEntryId(
  row: Pick<SessionRow, 'leafEntryId'>,
  entries: Array<Pick<EntryRow, 'entryId'>>,
): string | null {
  if (row.leafEntryId === null) {
    return null;
  }

  if (row.leafEntryId && entries.some(entry => entry.entryId === row.leafEntryId)) {
    return row.leafEntryId;
  }

  return entries[entries.length - 1]?.entryId ?? null;
}

function mapEntryRowToEntry(row: EntryRow): SessionTreeEntry {
  const base = {
    id: row.entryId,
    parentId: row.parentEntryId ?? null,
    timestamp: row.timestamp,
    metadata: toOptionalRecord(row.metadata),
  };
  const payload = cloneJsonValue(row.payload) ?? {};

  switch (row.type) {
    case 'message':
      return {
        ...base,
        type: 'message',
        message: expectMessage(payload.message, row),
      };
    case 'thinking_level_change':
      return {
        ...base,
        type: 'thinking_level_change',
        thinkingLevel: expectString(payload.thinkingLevel, row, 'thinkingLevel'),
      };
    case 'model_change':
      return {
        ...base,
        type: 'model_change',
        provider: expectString(payload.provider, row, 'provider'),
        modelId: expectString(payload.modelId, row, 'modelId'),
      };
    case 'compaction':
      return {
        ...base,
        type: 'compaction',
        summary: expectString(payload.summary, row, 'summary'),
        firstKeptEntryId: expectString(payload.firstKeptEntryId, row, 'firstKeptEntryId'),
        tokensBefore: expectNumber(payload.tokensBefore, row, 'tokensBefore'),
        details: cloneJsonValue(payload.details) as Record<string, unknown> | undefined,
        fromHook: expectOptionalBoolean(payload.fromHook, row, 'fromHook'),
      };
    case 'branch_summary':
      return {
        ...base,
        type: 'branch_summary',
        fromId: expectString(payload.fromId, row, 'fromId'),
        summary: expectString(payload.summary, row, 'summary'),
        details: cloneJsonValue(payload.details) as Record<string, unknown> | undefined,
        fromHook: expectOptionalBoolean(payload.fromHook, row, 'fromHook'),
      };
    case 'custom':
      return {
        ...base,
        type: 'custom',
        customType: expectString(payload.customType, row, 'customType'),
        data: cloneJsonValue(payload.data) as Record<string, unknown> | undefined,
      };
    case 'custom_message':
      return {
        ...base,
        type: 'custom_message',
        customType: expectString(payload.customType, row, 'customType'),
        content: cloneJsonValue(payload.content),
        details: cloneJsonValue(payload.details) as Record<string, unknown> | undefined,
        display: expectBoolean(payload.display, row, 'display'),
      };
    case 'subagent_batch':
      return {
        ...base,
        type: 'subagent_batch',
        content: expectSubagentBatchContent(payload.content, row),
      };
    case 'subagent_result':
      return {
        ...base,
        type: 'subagent_result',
        content: expectSubagentResultContent(payload.content, row),
      };
    case 'label':
      return {
        ...base,
        type: 'label',
        targetId: expectString(payload.targetId, row, 'targetId'),
        label: Object.prototype.hasOwnProperty.call(payload, 'label')
          ? expectOptionalString(payload.label, row, 'label')
          : undefined,
      };
    case 'session_info':
      return {
        ...base,
        type: 'session_info',
        name: expectOptionalString(payload.name, row, 'name'),
      };
    default:
      throw new SessionTreeProviderError(`Unsupported entry type ${row.type} in session ${row.sessionId}`);
  }
}

function mapEntryToRow(sessionId: string, entry: SessionTreeEntry, now: string, entrySeq: number): EntryRow {
  const base: EntryRow = {
    sessionId,
    entryId: entry.id,
    parentEntryId: entry.parentId,
    entrySeq,
    timestamp: entry.timestamp,
    type: entry.type,
    metadata: cloneJsonValue(entry.metadata) ?? null,
    payload: null,
    createTime: now,
    updateTime: now,
  };

  switch (entry.type) {
    case 'message':
      return {
        ...base,
        payload: {
          message: cloneJsonValue(entry.message),
        },
      };
    case 'thinking_level_change':
      return {
        ...base,
        payload: {
          thinkingLevel: entry.thinkingLevel,
        },
      };
    case 'model_change':
      return {
        ...base,
        payload: {
          provider: entry.provider,
          modelId: entry.modelId,
        },
      };
    case 'compaction':
      return {
        ...base,
        payload: compactObject({
          summary: entry.summary,
          firstKeptEntryId: entry.firstKeptEntryId,
          tokensBefore: entry.tokensBefore,
          details: cloneJsonValue(entry.details),
          fromHook: entry.fromHook,
        }),
      };
    case 'branch_summary':
      return {
        ...base,
        payload: compactObject({
          fromId: entry.fromId,
          summary: entry.summary,
          details: cloneJsonValue(entry.details),
          fromHook: entry.fromHook,
        }),
      };
    case 'custom':
      return {
        ...base,
        payload: compactObject({
          customType: entry.customType,
          data: cloneJsonValue(entry.data),
        }),
      };
    case 'custom_message':
      return {
        ...base,
        payload: compactObject({
          customType: entry.customType,
          content: cloneJsonValue(entry.content),
          details: cloneJsonValue(entry.details),
          display: entry.display,
        }),
      };
    case 'subagent_batch':
      return {
        ...base,
        payload: compactObject({
          content: cloneJsonValue(entry.content),
        }),
      };
    case 'subagent_result':
      return {
        ...base,
        payload: compactObject({
          content: cloneJsonValue(entry.content),
        }),
      };
    case 'label':
      return {
        ...base,
        payload: compactObject({
          targetId: entry.targetId,
          label: entry.label,
        }),
      };
    case 'session_info':
      return {
        ...base,
        payload: compactObject({
          name: entry.name,
        }),
      };
    default: {
      const exhaustive: never = entry;
      return exhaustive;
    }
  }
}

function nextEntrySeq(entries: SessionTreeEntry[]): number {
  return entries.length;
}

function assertAppendParentConstraint(
  entries: SessionTreeEntry[],
  sessionId: string,
  parentId: string | null,
): void {
  if (parentId === null) {
    if (entries.some(entry => entry.parentId === null)) {
      throw new SessionTreeProviderError(`Session ${sessionId} already has a root entry`);
    }
    return;
  }

  if (!entries.some(entry => entry.id === parentId)) {
    throw new SessionTreeProviderError(`Parent entry ${parentId} not found in session ${sessionId}`);
  }
}

function assertImmutableEntryPatch(entry: SessionTreeEntry, patch: Record<string, unknown>): void {
  const immutableKeys: Array<keyof Pick<SessionTreeEntry, 'id' | 'type' | 'parentId' | 'timestamp'>> = [
    'id',
    'type',
    'parentId',
    'timestamp',
  ];

  for (const key of immutableKeys) {
    if (Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== entry[key]) {
      throw new SessionTreeProviderError(`Entry field ${key} is immutable`);
    }
  }

  const allowedKeys = getAllowedPatchKeys(entry.type);
  for (const key of Object.keys(patch)) {
    if (!allowedKeys.has(key)) {
      throw new SessionTreeProviderError(`Entry field ${key} cannot be updated for entry type ${entry.type}`);
    }
  }
}

function getAllowedPatchKeys(entryType: SessionTreeEntry['type']): ReadonlySet<string> {
  switch (entryType) {
    case 'message':
      return new Set(['message', 'metadata']);
    case 'custom_message':
    case 'subagent_batch':
    case 'subagent_result':
      return new Set(['content', 'details', 'display', 'metadata']);
    case 'custom':
      return new Set(['data', 'metadata']);
    case 'thinking_level_change':
    case 'model_change':
    case 'compaction':
    case 'branch_summary':
    case 'label':
    case 'session_info':
      return new Set(['metadata']);
    default: {
      const exhaustive: never = entryType;
      return exhaustive;
    }
  }
}

function cloneSnapshot(snapshot: SessionTreeSnapshot): SessionTreeSnapshot {
  return {
    session: cloneSession(snapshot.session),
    entries: snapshot.entries.map(cloneEntry),
    activePath: snapshot.activePath.map(cloneEntry),
    childrenByParentId: cloneJsonValue(snapshot.childrenByParentId),
    labelsByEntryId: cloneJsonValue(snapshot.labelsByEntryId),
    runtimeContext: cloneJsonValue(snapshot.runtimeContext),
  };
}

function cloneSession(session: SessionTreeSession): SessionTreeSession {
  return cloneJsonValue(session);
}

function cloneEntry(entry: SessionTreeEntry): SessionTreeEntry {
  return cloneJsonValue(entry);
}

function compactObject(value: Record<string, unknown>): Record<string, unknown> {
  const result: Record<string, unknown> = {};

  for (const [key, nested] of Object.entries(value)) {
    if (nested !== undefined) {
      result[key] = nested;
    }
  }

  return result;
}

function cloneJsonValue<T>(value: T): T {
  if (value === undefined) {
    return value;
  }
  assertJsonSerializable(value, 'cloneJsonValue input');
  return JSON.parse(JSON.stringify(value)) as T;
}

function normalizeTimestamp(value: string | Date | undefined): string | undefined {
  if (value === undefined) {
    return undefined;
  }

  if (typeof value === 'string') {
    const normalized = normalizeMySqlTimestamp(value);
    if (normalized) {
      return normalized;
    }
    throw new SessionTreeProviderError(`Invalid timestamp value ${value}`);
  }

  return value.toISOString();
}

function toNullable(value: string | undefined): string | null {
  return value ?? null;
}

function toUndefined<T>(value: T | null | undefined): T | undefined {
  return value === null ? undefined : value;
}

function normalizeIdentityValue(value: string | number | undefined): string | null {
  if (value === undefined) {
    return null;
  }
  return String(value);
}

function toOptionalRecord(value: Record<string, unknown> | null | undefined): Record<string, unknown> | undefined {
  if (value === null || value === undefined) {
    return undefined;
  }

  return cloneJsonValue(value);
}

function expectString(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
  field: string,
): string {
  if (typeof value === 'string') {
    return value;
  }
  throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing ${field}`);
}

function expectNumber(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
  field: string,
): number {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }
  throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing ${field}`);
}

function expectBoolean(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
  field: string,
): boolean {
  if (typeof value === 'boolean') {
    return value;
  }
  throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing ${field}`);
}

function expectOptionalBoolean(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
  field: string,
): boolean | undefined {
  if (value === undefined) {
    return undefined;
  }
  return expectBoolean(value, row, field);
}

function expectOptionalString(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
  field: string,
): string | undefined {
  if (value === undefined) {
    return undefined;
  }
  return expectString(value, row, field);
}

function expectOptionalNullableString(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
  field: string,
): string | null | undefined {
  if (value === undefined) {
    return undefined;
  }

  if (value === null) {
    return null;
  }

  return expectString(value, row, field);
}

function expectRecordArray(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
  field: string,
): Record<string, unknown>[] {
  if (!Array.isArray(value)) {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing ${field}`);
  }

  return value.map((item, index) => {
    if (!isPlainObject(item)) {
      throw new SessionTreeProviderError(
        `Entry ${row.entryId} in session ${row.sessionId} has invalid ${field}[${index}]`,
      );
    }

    return cloneJsonValue(item);
  });
}

function expectMessage(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
): Extract<SessionTreeEntry, { type: 'message' }>['message'] {
  if (!isPlainObject(value)) {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing message`);
  }

  const role = value.role;
  if (typeof role !== 'string') {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing message.role`);
  }

  if (!Object.prototype.hasOwnProperty.call(value, 'content')) {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing message.content`);
  }

  assertJsonSerializable(value, `message payload for entry ${row.entryId} in session ${row.sessionId}`);
  return cloneJsonValue(value) as Extract<SessionTreeEntry, { type: 'message' }>['message'];
}

function expectSubagentBatchContent(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
): Extract<SessionTreeEntry, { type: 'subagent_batch' }>['content'] {
  if (!isPlainObject(value)) {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing content`);
  }

  const batchId = expectString(value.batchId, row, 'content.batchId');
  const mode = value.mode;
  if (mode !== 'single' && mode !== 'parallel') {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} has invalid content.mode`);
  }

  const status = value.status;
  if (status !== 'running' && status !== 'completed') {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} has invalid content.status`);
  }

  return compactObject({
    batchId,
    mode,
    tasks: expectRecordArray(value.tasks, row, 'content.tasks'),
    status,
    parentEntryId: expectOptionalNullableString(value.parentEntryId, row, 'content.parentEntryId'),
  }) as Extract<SessionTreeEntry, { type: 'subagent_batch' }>['content'];
}

function expectSubagentResultContent(
  value: unknown,
  row: Pick<EntryRow, 'sessionId' | 'entryId'>,
): Extract<SessionTreeEntry, { type: 'subagent_result' }>['content'] {
  if (!isPlainObject(value)) {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} is missing content`);
  }

  const batchId = expectString(value.batchId, row, 'content.batchId');
  const status = value.status;
  if (status !== 'running' && status !== 'completed') {
    throw new SessionTreeProviderError(`Entry ${row.entryId} in session ${row.sessionId} has invalid content.status`);
  }

  return compactObject({
    batchId,
    results: expectRecordArray(value.results, row, 'content.results'),
    status,
    parentEntryId: expectOptionalNullableString(value.parentEntryId, row, 'content.parentEntryId'),
  }) as Extract<SessionTreeEntry, { type: 'subagent_result' }>['content'];
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    return false;
  }

  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

type IdentityType = 'number' | 'string';

type SessionIdentityMetadata = {
  agentIdType?: IdentityType;
  userIdType?: IdentityType;
};

function buildSessionMetadata(
  metadata: Record<string, unknown> | undefined,
  agentId: string | number | undefined,
  userId: string | number | undefined,
): Record<string, unknown> | null {
  const base = cloneJsonValue(metadata) ?? {};
  const identityMeta: SessionIdentityMetadata = compactObject({
    agentIdType: getIdentityType(agentId),
    userIdType: getIdentityType(userId),
  }) as SessionIdentityMetadata;

  if (Object.keys(identityMeta).length === 0) {
    return Object.keys(base).length > 0 ? base : null;
  }

  return {
    ...base,
    __sessionTreeIdentity: identityMeta,
  };
}

function getSessionIdentityMetadata(metadata: Record<string, unknown> | null | undefined): SessionIdentityMetadata {
  if (!isPlainObject(metadata?.__sessionTreeIdentity)) {
    return {};
  }

  const raw = metadata.__sessionTreeIdentity;
  return {
    agentIdType: raw.agentIdType === 'number' || raw.agentIdType === 'string' ? raw.agentIdType : undefined,
    userIdType: raw.userIdType === 'number' || raw.userIdType === 'string' ? raw.userIdType : undefined,
  };
}

function stripSessionIdentityMetadata(
  metadata: Record<string, unknown> | null | undefined,
): Record<string, unknown> | undefined {
  if (!metadata) {
    return undefined;
  }

  const cloned = cloneJsonValue(metadata);
  delete cloned.__sessionTreeIdentity;
  return Object.keys(cloned).length > 0 ? cloned : undefined;
}

function getIdentityType(value: string | number | undefined): IdentityType | undefined {
  if (typeof value === 'number') {
    return 'number';
  }

  if (typeof value === 'string') {
    return 'string';
  }

  return undefined;
}

function restoreIdentityValue(
  raw: string | null | undefined,
  type: IdentityType | undefined,
): string | number | undefined {
  if (raw === null || raw === undefined) {
    return undefined;
  }

  if (type === 'number') {
    const parsed = Number(raw);
    if (Number.isFinite(parsed)) {
      return parsed;
    }
  }

  return raw;
}

function withExistingPrimaryKey<T extends { id?: number }>(existing: T, next: T): T {
  if (typeof existing.id === 'number') {
    return {
      ...next,
      id: existing.id,
    };
  }

  return next;
}

function normalizeMySqlTimestamp(value: string): string | null {
  const parsed = Date.parse(value.includes('T') ? value : `${value.replace(' ', 'T')}Z`);
  if (Number.isNaN(parsed)) {
    return null;
  }
  return new Date(parsed).toISOString();
}

function assertJsonSerializable(value: unknown, context: string, path = '$'): void {
  if (value === undefined || value === null) {
    return;
  }

  const valueType = typeof value;
  if (valueType === 'string' || valueType === 'boolean') {
    return;
  }

  if (valueType === 'number') {
    if (Number.isFinite(value)) {
      return;
    }
    throw new SessionTreeProviderError(`${context} contains a non-finite number at ${path}`);
  }

  if (valueType === 'bigint' || valueType === 'function' || valueType === 'symbol') {
    throw new SessionTreeProviderError(`${context} contains non-JSON-serializable ${valueType} at ${path}`);
  }

  if (Array.isArray(value)) {
    value.forEach((item, index) => {
      if (item === undefined) {
        throw new SessionTreeProviderError(`${context} contains undefined array item at ${path}[${index}]`);
      }
      assertJsonSerializable(item, context, `${path}[${index}]`);
    });
    return;
  }

  if (valueType === 'object') {
    const prototype = Object.getPrototypeOf(value);
    if (prototype !== Object.prototype && prototype !== null) {
      const constructorName = prototype?.constructor?.name ?? 'object';
      throw new SessionTreeProviderError(`${context} contains non-plain object ${constructorName} at ${path}`);
    }

    for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
      if (nested === undefined) {
        continue;
      }
      assertJsonSerializable(nested, context, `${path}.${key}`);
    }
    return;
  }

  throw new SessionTreeProviderError(`${context} contains unsupported value at ${path}`);
}

function createDefaultSessionId(): string {
  return randomUUID();
}

function createDefaultEntryId(existingIds?: { has(id: string): boolean }): string {
  while (true) {
    const id = `entry_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
    if (!existingIds?.has(id)) {
      return id;
    }
  }
}
