/**
 * Session Channel Reducer — Pure reducer for `SessionState`, including all
 * session-internal helpers (turn lifecycle, tool call transitions, pending
 * messages, input requests).
 *
 * @module channels-session/reducer
 */

import { ActionType } from '../common/actions.js';
import type {
  SessionInputRequest,
  SessionState,
  ToolCallState,
  ResponsePart,
  ToolCallResponsePart,
  Turn,
  PendingMessage,
  ConfirmationOption,
  McpServerCustomization,
} from './state.js';
import {
  SessionLifecycle,
  SessionStatus,
  TurnState,
  ToolCallStatus,
  ToolCallConfirmationReason,
  ToolCallCancellationReason,
  ResponsePartKind,
  PendingMessageKind,
  CustomizationType,
} from './state.js';
import type { SessionAction } from '../action-origin.generated.js';
import { softAssertNever } from '../common/reducer-helpers.js';

// ─── Helpers ─────────────────────────────────────────────────────────────────

/** Extracts the common base fields shared by all tool call lifecycle states. */
function tcBase(tc: ToolCallState) {
  return {
    toolCallId: tc.toolCallId,
    toolName: tc.toolName,
    displayName: tc.displayName,
    contributor: tc.contributor,
    _meta: tc._meta,
  };
}

function tcBaseWithMeta(tc: ToolCallState, meta: Record<string, unknown> | undefined) {
  return {
    ...tcBase(tc),
    _meta: meta ?? tc._meta,
  };
}

/** Resolves a selected option from the confirmation options array by ID. */
function resolveSelectedOption(options: ConfirmationOption[] | undefined, id: string | undefined): ConfirmationOption | undefined {
  if (!id || !options) {
    return undefined;
  }
  return options.find(o => o.id === id);
}

/** Returns `true` if the active turn has any tool call awaiting user confirmation. */
function hasPendingToolCallConfirmation(state: SessionState): boolean {
  if (!state.activeTurn) {
    return false;
  }
  return state.activeTurn.responseParts.some(part =>
    part.kind === ResponsePartKind.ToolCall
    && (part.toolCall.status === ToolCallStatus.PendingConfirmation
      || part.toolCall.status === ToolCallStatus.PendingResultConfirmation),
  );
}

/** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */
const STATUS_ACTIVITY_MASK = (1 << 5) - 1;

/** Sets or clears a metadata flag on a status value. */
function withStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean): SessionStatus {
  return set ? status | flag : status & ~flag;
}

/** Derives the summary status from live session work, preserving orthogonal flags. */
function summaryStatus(state: SessionState, terminalStatus?: SessionStatus.Error): SessionStatus {
  let activity: SessionStatus;
  if (terminalStatus) {
    activity = terminalStatus;
  } else if ((state.inputRequests?.length ?? 0) > 0 || hasPendingToolCallConfirmation(state)) {
    activity = SessionStatus.InputNeeded;
  } else if (state.activeTurn) {
    activity = SessionStatus.InProgress;
  } else {
    activity = SessionStatus.Idle;
  }

  return state.summary.status & ~STATUS_ACTIVITY_MASK | activity;
}

/**
 * Returns a state with `summary.status` recomputed. Use this after reducers
 * that change data which feeds into {@link summaryStatus} (e.g. tool call
 * lifecycle transitions that may enter or leave a pending-confirmation state).
 */
function refreshSummaryStatus(state: SessionState): SessionState {
  const status = summaryStatus(state);
  if (status === state.summary.status) {
    return state;
  }
  return { ...state, summary: { ...state.summary, status } };
}

/**
 * Ends the active turn, finalizing it into a completed turn record.
 *
 * Tool call parts with non-terminal states are forced to cancelled.
 * Pending permissions are stripped from tool call parts.
 */
function endTurn(
  state: SessionState,
  turnId: string,
  turnState: TurnState,
  terminalStatus?: SessionStatus.Error,
  error?: { errorType: string; message: string; stack?: string },
): SessionState {
  if (!state.activeTurn || state.activeTurn.id !== turnId) {
    return state;
  }
  const active = state.activeTurn;

  const responseParts: ResponsePart[] = active.responseParts.map(part => {
    if (part.kind !== ResponsePartKind.ToolCall) {
      return part;
    }
    const tc = part.toolCall;
    if (tc.status === ToolCallStatus.Completed || tc.status === ToolCallStatus.Cancelled) {
      return part;
    }
    // Force non-terminal tool calls into cancelled state
    return {
      kind: ResponsePartKind.ToolCall,
      toolCall: {
        status: ToolCallStatus.Cancelled as const,
        ...tcBase(tc),
        invocationMessage: tc.status === ToolCallStatus.Streaming ? (tc.invocationMessage ?? '') : tc.invocationMessage,
        toolInput: tc.status === ToolCallStatus.Streaming ? undefined : tc.toolInput,
        reason: ToolCallCancellationReason.Skipped,
      },
    };
  });

  const turn: Turn = {
    id: active.id,
    message: active.message,
    responseParts,
    usage: active.usage,
    state: turnState,
    error,
  };

  const next: SessionState = {
    ...state,
    turns: [...state.turns, turn],
    activeTurn: undefined,
    summary: { ...state.summary, modifiedAt: Date.now() },
  };
  delete next.inputRequests;
  return {
    ...next,
    summary: { ...next.summary, status: summaryStatus(next, terminalStatus) },
  };
}

function upsertInputRequest(state: SessionState, request: SessionInputRequest): SessionState {
  const existing = state.inputRequests ?? [];
  const idx = existing.findIndex(r => r.id === request.id);
  const inputRequests = [...existing];
  if (idx >= 0) {
    const answers = request.answers ?? inputRequests[idx].answers;
    inputRequests[idx] = { ...request, answers };
  } else {
    inputRequests.push(request);
  }
  const next = { ...state, inputRequests };
  return { ...next, summary: { ...next.summary, status: withStatusFlag(summaryStatus(next), SessionStatus.IsRead, false), modifiedAt: Date.now() } };
}

/**
 * Immutably updates the tool call inside a `ToolCall` response part in the
 * active turn's `responseParts` array. Returns `state` unchanged if the
 * active turn or tool call doesn't match.
 */
function updateToolCallInParts(
  state: SessionState,
  turnId: string,
  toolCallId: string,
  updater: (tc: ToolCallState) => ToolCallState,
): SessionState {
  const activeTurn = state.activeTurn;
  if (!activeTurn || activeTurn.id !== turnId) {
    return state;
  }

  let found = false;
  const responseParts = activeTurn.responseParts.map(part => {
    if (part.kind === ResponsePartKind.ToolCall && part.toolCall.toolCallId === toolCallId) {
      const updated = updater(part.toolCall);
      if (updated === part.toolCall) {
        return part;
      }
      found = true;
      return { ...part, toolCall: updated };
    }
    return part;
  });

  if (!found) {
    return state;
  }

  return {
    ...state,
    activeTurn: { ...activeTurn, responseParts },
  };
}

/**
 * Immutably updates a response part by `partId` in the active turn.
 * For markdown/reasoning parts, matches on `id`. For tool call parts,
 * matches on `toolCall.toolCallId`.
 */
function updateResponsePart(
  state: SessionState,
  turnId: string,
  partId: string,
  updater: (part: ResponsePart) => ResponsePart,
): SessionState {
  const activeTurn = state.activeTurn;
  if (!activeTurn || activeTurn.id !== turnId) {
    return state;
  }

  let found = false;
  const responseParts = activeTurn.responseParts.map(part => {
    if (!found) {
      const id = part.kind === ResponsePartKind.ToolCall
        ? part.toolCall.toolCallId
        : 'id' in part ? part.id : undefined;
      if (id === partId) {
        found = true;
        return updater(part);
      }
    }
    return part;
  });

  if (!found) {
    return state;
  }

  return {
    ...state,
    activeTurn: { ...activeTurn, responseParts },
  };
}

// ─── Session Reducer ─────────────────────────────────────────────────────────

/**
 * Pure reducer for session state. Handles all {@link SessionAction} variants.
 */
export function sessionReducer(state: SessionState, action: SessionAction, log?: (msg: string) => void): SessionState {
  switch (action.type) {
    // ── Lifecycle ──────────────────────────────────────────────────────────

    case ActionType.SessionReady:
      // `SessionReady` is purely a lifecycle transition (Creating ->
      // Ready). It must not touch `summary.status`: for provisional
      // sessions the first turn can start before materialization
      // completes, so an `activeTurn` may already be set when this
      // action is dispatched (e.g. from a materialize-session handler).
      // Other reducers keep `summary.status` in sync with the activity
      // state via `summaryStatus`/`refreshSummaryStatus`, so leaving it
      // alone here is correct.
      return { ...state, lifecycle: SessionLifecycle.Ready };

    case ActionType.SessionCreationFailed:
      return {
        ...state,
        lifecycle: SessionLifecycle.CreationFailed,
        creationError: action.error,
      };

    // ── Turn Lifecycle ────────────────────────────────────────────────────

    case ActionType.SessionTurnStarted: {
      let next: SessionState = {
        ...state,
        activeTurn: {
          id: action.turnId,
          message: action.message,
          responseParts: [],
          usage: undefined,
        },
      };
      next = {
        ...next,
        summary: { ...next.summary, status: withStatusFlag(summaryStatus(next), SessionStatus.IsRead, false), modifiedAt: Date.now() },
      };

      // If this turn was auto-started from a pending message, remove it
      if (action.queuedMessageId) {
        if (next.steeringMessage?.id === action.queuedMessageId) {
          next = { ...next, steeringMessage: undefined };
        }
        if (next.queuedMessages) {
          const filtered = next.queuedMessages.filter(m => m.id !== action.queuedMessageId);
          next = { ...next, queuedMessages: filtered.length > 0 ? filtered : undefined };
        }
      }

      return next;
    }

    case ActionType.SessionDelta:
      return updateResponsePart(state, action.turnId, action.partId, part => {
        if (part.kind === ResponsePartKind.Markdown) {
          return { ...part, content: part.content + action.content };
        }
        return part;
      });

    case ActionType.SessionResponsePart:
      if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
        return state;
      }
      return {
        ...state,
        activeTurn: {
          ...state.activeTurn,
          responseParts: [...state.activeTurn.responseParts, action.part],
        },
      };

    case ActionType.SessionTurnComplete:
      return endTurn(state, action.turnId, TurnState.Complete);

    case ActionType.SessionTurnCancelled:
      return endTurn(state, action.turnId, TurnState.Cancelled);

    case ActionType.SessionError:
      return endTurn(state, action.turnId, TurnState.Error, SessionStatus.Error, action.error);

    // ── Tool Call State Machine ───────────────────────────────────────────

    case ActionType.SessionToolCallStart:
      if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
        return state;
      }
      return {
        ...state,
        activeTurn: {
          ...state.activeTurn,
          responseParts: [
            ...state.activeTurn.responseParts,
            {
              kind: ResponsePartKind.ToolCall,
              toolCall: {
                toolCallId: action.toolCallId,
                toolName: action.toolName,
                displayName: action.displayName,
                contributor: action.contributor,
                _meta: action._meta,
                status: ToolCallStatus.Streaming,
              },
            } satisfies ToolCallResponsePart,
          ],
        },
      };

    case ActionType.SessionToolCallDelta:
      return updateToolCallInParts(state, action.turnId, action.toolCallId, tc => {
        if (tc.status !== ToolCallStatus.Streaming) {
          return tc;
        }
        return {
          ...tc,
          ...(action._meta !== undefined ? { _meta: action._meta } : {}),
          partialInput: (tc.partialInput ?? '') + action.content,
          invocationMessage: action.invocationMessage ?? tc.invocationMessage,
        };
      });

    case ActionType.SessionToolCallReady:
      return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => {
        if (tc.status !== ToolCallStatus.Streaming && tc.status !== ToolCallStatus.Running) {
          return tc;
        }
        const base = tcBaseWithMeta(tc, action._meta);
        if (action.confirmed) {
          return {
            status: ToolCallStatus.Running,
            ...base,
            invocationMessage: action.invocationMessage,
            toolInput: action.toolInput,
            confirmed: action.confirmed,
          };
        }
        return {
          status: ToolCallStatus.PendingConfirmation,
          ...base,
          invocationMessage: action.invocationMessage,
          toolInput: action.toolInput,
          confirmationTitle: action.confirmationTitle,
          edits: action.edits,
          editable: action.editable,
          ...(action.options ? { options: action.options } : {}),
        };
      }));

    case ActionType.SessionToolCallConfirmed:
      return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => {
        if (tc.status !== ToolCallStatus.PendingConfirmation) {
          return tc;
        }
        const base = tcBaseWithMeta(tc, action._meta);
        const selectedOption = resolveSelectedOption(tc.options, action.selectedOptionId);
        if (action.approved) {
          return {
            status: ToolCallStatus.Running,
            ...base,
            invocationMessage: tc.invocationMessage,
            toolInput: action.editedToolInput ?? tc.toolInput,
            confirmed: action.confirmed,
            ...(selectedOption ? { selectedOption } : {}),
          };
        }
        return {
          status: ToolCallStatus.Cancelled,
          ...base,
          invocationMessage: tc.invocationMessage,
          toolInput: tc.toolInput,
          reason: action.reason,
          reasonMessage: action.reasonMessage,
          userSuggestion: action.userSuggestion,
          ...(selectedOption ? { selectedOption } : {}),
        };
      }));

    case ActionType.SessionToolCallComplete:
      return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => {
        if (tc.status !== ToolCallStatus.Running && tc.status !== ToolCallStatus.PendingConfirmation) {
          return tc;
        }
        const base = tcBaseWithMeta(tc, action._meta);
        const confirmed = tc.status === ToolCallStatus.Running
          ? tc.confirmed
          : ToolCallConfirmationReason.NotNeeded;
        const selectedOption = tc.status === ToolCallStatus.Running
          ? tc.selectedOption
          : undefined;
        if (action.requiresResultConfirmation) {
          return {
            status: ToolCallStatus.PendingResultConfirmation,
            ...base,
            invocationMessage: tc.invocationMessage,
            toolInput: tc.toolInput,
            confirmed,
            ...(selectedOption ? { selectedOption } : {}),
            ...action.result,
          };
        }
        return {
          status: ToolCallStatus.Completed,
          ...base,
          invocationMessage: tc.invocationMessage,
          toolInput: tc.toolInput,
          confirmed,
          ...(selectedOption ? { selectedOption } : {}),
          ...action.result,
        };
      }));

    case ActionType.SessionToolCallResultConfirmed:
      return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => {
        if (tc.status !== ToolCallStatus.PendingResultConfirmation) {
          return tc;
        }
        const base = tcBaseWithMeta(tc, action._meta);
        if (action.approved) {
          return {
            status: ToolCallStatus.Completed,
            ...base,
            invocationMessage: tc.invocationMessage,
            toolInput: tc.toolInput,
            confirmed: tc.confirmed,
            ...(tc.selectedOption ? { selectedOption: tc.selectedOption } : {}),
            success: tc.success,
            pastTenseMessage: tc.pastTenseMessage,
            content: tc.content,
            structuredContent: tc.structuredContent,
            error: tc.error,
          };
        }
        return {
          status: ToolCallStatus.Cancelled,
          ...base,
          invocationMessage: tc.invocationMessage,
          toolInput: tc.toolInput,
          reason: ToolCallCancellationReason.ResultDenied,
          ...(tc.selectedOption ? { selectedOption: tc.selectedOption } : {}),
        };
      }));

    case ActionType.SessionToolCallContentChanged:
      return updateToolCallInParts(state, action.turnId, action.toolCallId, tc => {
        if (tc.status !== ToolCallStatus.Running) {
          return tc;
        }
        return {
          ...tc,
          ...(action._meta !== undefined ? { _meta: action._meta } : {}),
          content: action.content,
        };
      });

    // ── Metadata ──────────────────────────────────────────────────────────

    case ActionType.SessionTitleChanged:
      return {
        ...state,
        summary: { ...state.summary, title: action.title, modifiedAt: Date.now() },
      };

    case ActionType.SessionUsage:
      if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
        return state;
      }
      return {
        ...state,
        activeTurn: { ...state.activeTurn, usage: action.usage },
      };

    case ActionType.SessionReasoning:
      return updateResponsePart(state, action.turnId, action.partId, part => {
        if (part.kind === ResponsePartKind.Reasoning) {
          return { ...part, content: part.content + action.content };
        }
        return part;
      });

    case ActionType.SessionModelChanged:
      return {
        ...state,
        summary: { ...state.summary, model: action.model, modifiedAt: Date.now() },
      };

    case ActionType.SessionAgentChanged:
      return {
        ...state,
        summary: { ...state.summary, agent: action.agent, modifiedAt: Date.now() },
      };

    case ActionType.SessionIsReadChanged:
      return {
        ...state,
        summary: { ...state.summary, status: withStatusFlag(state.summary.status, SessionStatus.IsRead, action.isRead) },
      };

    case ActionType.SessionIsArchivedChanged:
      return {
        ...state,
        summary: { ...state.summary, status: withStatusFlag(state.summary.status, SessionStatus.IsArchived, action.isArchived) },
      };

    case ActionType.SessionActivityChanged:
      return {
        ...state,
        summary: { ...state.summary, activity: action.activity },
      };

    case ActionType.SessionChangesetsChanged: {
      const { changesets: _omit, ...stateWithoutChangesets } = state;
      return action.changesets
        ? { ...stateWithoutChangesets, changesets: action.changesets }
        : stateWithoutChangesets;
    }

    case ActionType.SessionConfigChanged:
      if (!state.config) {
        return state;
      }
      return {
        ...state,
        config: {
          ...state.config,
          values: action.replace ? { ...action.config } : { ...state.config.values, ...action.config },
        },
        summary: {
          ...state.summary,
          modifiedAt: Date.now(),
        },
      };

    case ActionType.SessionMetaChanged:
      return { ...state, _meta: action._meta };

    case ActionType.SessionServerToolsChanged:
      return { ...state, serverTools: action.tools };

    case ActionType.SessionActiveClientChanged:
      return {
        ...state,
        activeClient: action.activeClient ?? undefined,
      };

    case ActionType.SessionActiveClientToolsChanged:
      if (!state.activeClient) {
        return state;
      }
      return {
        ...state,
        activeClient: { ...state.activeClient, tools: action.tools },
      };

    // ── Customizations ──────────────────────────────────────────────────

    case ActionType.SessionCustomizationsChanged:
      return { ...state, customizations: action.customizations };

    case ActionType.SessionCustomizationToggled: {
      const list = state.customizations;
      if (!list) {
        return state;
      }
      const idx = list.findIndex(c => c.id === action.id);
      if (idx < 0) {
        return state;
      }
      const updated = [...list];
      updated[idx] = { ...list[idx], enabled: action.enabled };
      return { ...state, customizations: updated };
    }

    case ActionType.SessionCustomizationUpdated: {
      const list = state.customizations ?? [];
      const idx = list.findIndex(c => c.id === action.customization.id);
      if (idx < 0) {
        return { ...state, customizations: [...list, action.customization] };
      }
      const updated = [...list];
      updated[idx] = action.customization;
      return { ...state, customizations: updated };
    }

    case ActionType.SessionCustomizationRemoved: {
      const list = state.customizations;
      if (!list) {
        return state;
      }
      const topIdx = list.findIndex(c => c.id === action.id);
      if (topIdx >= 0) {
        const updated = list.slice();
        updated.splice(topIdx, 1);
        return { ...state, customizations: updated };
      }
      let changed = false;
      const updated = list.map(container => {
        if (container.type === CustomizationType.McpServer) {
          return container;
        }
        const children = container.children;
        if (!children) {
          return container;
        }
        const childIdx = children.findIndex(c => c.id === action.id);
        if (childIdx < 0) {
          return container;
        }
        changed = true;
        const newChildren = children.slice();
        newChildren.splice(childIdx, 1);
        return { ...container, children: newChildren };
      });
      if (!changed) {
        return state;
      }
      return { ...state, customizations: updated };
    }

    case ActionType.SessionMcpServerStateChanged: {
      const list = state.customizations;
      if (!list) {
        return state;
      }
      const topIdx = list.findIndex(c => c.id === action.id);
      if (topIdx >= 0) {
        const entry = list[topIdx];
        if (entry.type !== CustomizationType.McpServer) {
          return state;
        }
        const updatedEntry: McpServerCustomization = {
          ...entry,
          state: action.state,
          channel: action.channel,
        };
        const updated = list.slice();
        updated[topIdx] = updatedEntry;
        return { ...state, customizations: updated };
      }
      let changed = false;
      const updated = list.map(container => {
        if (container.type === CustomizationType.McpServer) {
          return container;
        }
        const children = container.children;
        if (!children) {
          return container;
        }
        const childIdx = children.findIndex(c => c.id === action.id);
        if (childIdx < 0) {
          return container;
        }
        const child = children[childIdx];
        if (child.type !== CustomizationType.McpServer) {
          return container;
        }
        changed = true;
        const updatedChild: McpServerCustomization = {
          ...child,
          state: action.state,
          channel: action.channel,
        };
        const newChildren = children.slice();
        newChildren[childIdx] = updatedChild;
        return { ...container, children: newChildren };
      });
      if (!changed) {
        return state;
      }
      return { ...state, customizations: updated };
    }

    // ── Truncation ────────────────────────────────────────────────────────

    case ActionType.SessionTruncated: {
      let turns: typeof state.turns;
      if (action.turnId === undefined) {
        turns = [];
      } else {
        const idx = state.turns.findIndex(t => t.id === action.turnId);
        if (idx < 0) {
          return state;
        }
        turns = state.turns.slice(0, idx + 1);
      }
      const next: SessionState = {
        ...state,
        turns,
        activeTurn: undefined,
        summary: { ...state.summary, modifiedAt: Date.now() },
      };
      delete next.inputRequests;
      return {
        ...next,
        summary: { ...next.summary, status: summaryStatus(next) },
      };
    }

    // ── Session Input Requests ─────────────────────────────────────────────

    case ActionType.SessionInputRequested:
      return upsertInputRequest(state, action.request);

    case ActionType.SessionInputAnswerChanged: {
      const existing = state.inputRequests;
      const idx = existing?.findIndex(request => request.id === action.requestId) ?? -1;
      if (!existing || idx < 0) {
        return state;
      }
      const request = existing[idx];
      const answers = { ...(request.answers ?? {}) };
      if (action.answer === undefined) {
        delete answers[action.questionId];
      } else {
        answers[action.questionId] = action.answer;
      }
      const updated = [...existing];
      updated[idx] = {
        ...request,
        answers: Object.keys(answers).length > 0 ? answers : undefined,
      };
      return {
        ...state,
        inputRequests: updated,
        summary: { ...state.summary, modifiedAt: Date.now() },
      };
    }

    case ActionType.SessionInputCompleted: {
      const existing = state.inputRequests;
      if (!existing?.some(request => request.id === action.requestId)) {
        return state;
      }
      const inputRequests = existing.filter(request => request.id !== action.requestId);
      const next: SessionState = {
        ...state,
      };
      if (inputRequests.length > 0) {
        next.inputRequests = inputRequests;
      } else {
        delete next.inputRequests;
      }
      return {
        ...next,
        summary: { ...next.summary, status: summaryStatus(next), modifiedAt: Date.now() },
      };
    }

    // ── Pending Messages ──────────────────────────────────────────────────

    case ActionType.SessionPendingMessageSet: {
      const entry: PendingMessage = { id: action.id, message: action.message };
      if (action.kind === PendingMessageKind.Steering) {
        return { ...state, steeringMessage: entry };
      }
      const existing = state.queuedMessages ?? [];
      const idx = existing.findIndex(m => m.id === action.id);
      if (idx >= 0) {
        const updated = [...existing];
        updated[idx] = entry;
        return { ...state, queuedMessages: updated };
      }
      return { ...state, queuedMessages: [...existing, entry] };
    }

    case ActionType.SessionPendingMessageRemoved: {
      if (action.kind === PendingMessageKind.Steering) {
        if (!state.steeringMessage || state.steeringMessage.id !== action.id) {
          return state;
        }
        return { ...state, steeringMessage: undefined };
      }
      const existing = state.queuedMessages;
      if (!existing) {
        return state;
      }
      const filtered = existing.filter(m => m.id !== action.id);
      return filtered.length === existing.length
        ? state
        : { ...state, queuedMessages: filtered.length > 0 ? filtered : undefined };
    }

    case ActionType.SessionQueuedMessagesReordered: {
      const existing = state.queuedMessages;
      if (!existing) {
        return state;
      }
      const byId = new Map(existing.map(m => [m.id, m]));
      const ordered = new Set<string>();
      const reordered = action.order
        .filter(id => {
          if (byId.has(id) && !ordered.has(id)) {
            ordered.add(id);
            return true;
          }
          return false;
        })
        .map(id => byId.get(id)!);
      // Append any messages not mentioned in order, preserving original order
      for (const m of existing) {
        if (!ordered.has(m.id)) {
          reordered.push(m);
        }
      }
      return { ...state, queuedMessages: reordered };
    }

    default:
      softAssertNever(action, log);
      return state;
  }
}
