import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import uuid from "react-uuid";
import { executeEditOperation, executeRedo, executeUndo, executeWorkflowCommand, lockField, lockItems, unlockItems, checkOperationImpact, checkItemReferences, } from "../services/editService";
import { DeleteItemsWarning } from "../DeleteItemsWarning";
import React, { useCallback, useMemo, useRef, useState, } from "react";
import { handleErrorResult } from "./helpers";
import { isMissingEditOperationResult } from "./editHistoryReconciliation";
import { useDebouncedCallback } from "use-debounce";
import { createOrUpdateSuggestedEdit, deleteSuggestedEdit, } from "../services/suggestedEditsService";
import { hasSuggestionContentChange, shouldRemoveNoOpSuggestion, } from "../reviews/suggestionContentChange";
import { GUID_REGEX_EXACT, getItemDescriptor } from "../utils";
import { cleanId } from "../utils/id-helper";
import { copyItemsNeedsTerminalCompletion, normalizeOpId, waitForEditOperationTerminal, } from "./waitForEditOperationTerminal";
import { isWorkflowMetadataField } from "../workflowFields";
import { ExternalChangesWarningContent, TreeOperationConfirmContent, UndoDifferentItemContent, } from "./OperationDialogContent";
import { isSuggestedEditRemoved, markSuggestedEditRemoved, upsertSuggestedEdit, } from "../reviews/suggestedEditState";
import { logFieldValueDiagnostic, summarizeFieldDescriptor, summarizeFieldValue, } from "../fieldValueDiagnostics";
import { getSuggestedEditItemLanguage, getSuggestedEditItemVersion, getSuggestionFieldIdentityKey, } from "../reviews/suggestionDisplayValue";
import { enqueueSuggestedEditFieldTask, waitForSuggestedEditFieldTasks, } from "./suggestedEditFieldQueue";
/** When true, the delete dialog shows "permanently delete (bypass recycle bin)". */
const ENABLE_HARD_DELETE_IN_DELETE_DIALOG = false;
// Track pending suggested edits to prevent creating duplicates before server response
const pendingSuggestedEdits = new Map();
const SUGGESTED_EDIT_SAVE_DEBOUNCE_MS = 500;
const FIELD_LOCK_REQUEST_TIMEOUT_MS = 10_000;
function createDelayedSuggestedEditSave(suggestedEdit) {
    return new Promise((resolve, reject) => {
        globalThis.setTimeout(() => {
            if (isSuggestedEditRemoved(suggestedEdit.id)) {
                // The suggestion was reverted to a no-op while this save was pending.
                // It never reached the server, so there is nothing to persist or delete.
                resolve({ type: "ignored" });
                return;
            }
            createOrUpdateSuggestedEdit(suggestedEdit).then(resolve, reject);
        }, SUGGESTED_EDIT_SAVE_DEBOUNCE_MS);
    });
}
/**
 * Determines if an operation modifies the content tree structure.
 * Tree-modifying operations require confirmation before undo/redo.
 */
function isTreeModifyingOperation(operation) {
    const treeModifyingTypes = [
        "create-item",
        "delete-items",
        "move-items",
        "copy-items",
        "clone-items",
        "import-items",
    ];
    return treeModifyingTypes.includes(operation.type);
}
function getRemovedComponentIds(operation) {
    if (operation.type !== "remove-component")
        return [];
    return (operation.componentIds ?? []).filter((id) => !!id);
}
function uniqueIds(ids) {
    return Array.from(new Set(ids));
}
function getFieldLockKey(field) {
    const itemId = cleanId(field.item.id) ?? field.item.id;
    const fieldId = cleanId(field.fieldId) ?? field.fieldId;
    return [
        itemId,
        field.item.language?.toLowerCase() ?? "",
        field.item.version,
        fieldId,
    ].join(":");
}
function fieldsMatchForLock(left, right) {
    if (!left || !right)
        return false;
    return (cleanId(left.fieldId) === cleanId(right.fieldId) &&
        cleanId(left.item.id) === cleanId(right.item.id) &&
        left.item.language === right.item.language &&
        left.item.version === right.item.version);
}
// Helper function to clean up pending operations for a deleted suggested edit
export function cleanupPendingSuggestedEditOperations(suggestedEdit, user) {
    const userKey = user?.name || "unknown";
    const pendingFieldKey = `${getSuggestionFieldIdentityKey({
        itemId: suggestedEdit.itemId,
        language: getSuggestedEditItemLanguage(suggestedEdit),
        version: getSuggestedEditItemVersion(suggestedEdit),
        fieldId: suggestedEdit.fieldId,
    })}:${userKey}`;
    // Remove from pending operations
    pendingSuggestedEdits.delete(pendingFieldKey);
}
export async function waitForPendingSuggestedEditSave(suggestedEdit) {
    const saveKey = getSuggestionFieldIdentityKey({
        itemId: suggestedEdit.itemId,
        language: getSuggestedEditItemLanguage(suggestedEdit),
        version: getSuggestedEditItemVersion(suggestedEdit),
        fieldId: suggestedEdit.fieldId,
    });
    await waitForSuggestedEditFieldTasks(saveKey);
}
export function getOperationsContext(state, ui) {
    const [editOperationExecuted, setEditOperationExecuted] = useState();
    const [executingEditOperations, setExecutingEditOperations] = useState([]);
    const lastOp = useRef(undefined);
    const undoing = useRef(false);
    // Track operation IDs that were successfully undone/redone but whose React state update
    // hasn't been rendered yet. Cleared on each render (when stateRef.current is up-to-date).
    const recentlyUndoneIds = useRef(new Set());
    const recentlyRedoneIds = useRef(new Set());
    const removingComponentIdsByOperationId = useRef(new Map());
    // Cache for field items (template field definitions) - these are static and don't change during editing
    // This prevents refetching on every keystroke since itemsRepository always refetches version 0
    const fieldItemsCache = useRef(new Map());
    const pendingFieldLocks = useRef(new Map());
    const acquiredFieldLockRef = useRef(state.lockedField);
    const stateRef = useRef(state);
    // Update ref synchronously during render (not via useEffect) to avoid stale state issues
    // This ensures stateRef.current always has the latest state when undo/redo is called
    stateRef.current = state;
    acquiredFieldLockRef.current = state.lockedField;
    // The render means React has processed pending state updates, so the editHistory in
    // stateRef is now authoritative. Clear the recently-processed tracking sets.
    recentlyUndoneIds.current.clear();
    recentlyRedoneIds.current.clear();
    // Extract values to avoid state object reference issues
    const itemsRepository = stateRef.current.itemsRepository;
    const clearInsertingIndicatorForOperation = useCallback((operationId) => {
        const key = normalizeOpId(operationId);
        stateRef.current.setInserting((current) => current?.operationId && normalizeOpId(current.operationId) === key
            ? undefined
            : current);
    }, []);
    const showRemovingIndicatorsForOperation = useCallback((operationId, componentIds) => {
        if (componentIds.length === 0)
            return;
        const key = normalizeOpId(operationId);
        removingComponentIdsByOperationId.current.set(key, componentIds);
        stateRef.current.setRemovingComponentIds((current) => uniqueIds([...current, ...componentIds]));
    }, []);
    const clearRemovingIndicatorsForOperation = useCallback((operationId) => {
        const key = normalizeOpId(operationId);
        const ids = removingComponentIdsByOperationId.current.get(key);
        if (!ids)
            return;
        removingComponentIdsByOperationId.current.delete(key);
        stateRef.current.setRemovingComponentIds((current) => {
            if (current.length === 0)
                return current;
            const activeRemovingIds = new Set();
            removingComponentIdsByOperationId.current.forEach((operationIds) => {
                operationIds.forEach((id) => activeRemovingIds.add(id));
            });
            return current.filter((id) => activeRemovingIds.has(id));
        });
    }, []);
    const executeOp = useCallback(async (op, options = {
        refresh: "immediate",
    }) => {
        console.log("Executing op", op);
        const removedComponentIds = getRemovedComponentIds(op);
        let keepRemovingIndicatorsUntilSocketCompletion = false;
        if (removedComponentIds.length > 0) {
            showRemovingIndicatorsForOperation(op.id, removedComponentIds);
        }
        setExecutingEditOperations((ops) => (ops ? [...ops, op] : [op]));
        try {
            const result = await executeEditOperation(op, stateRef.current.sessionId);
            // console.log("Result", result);
            if (!handleResult(result, options)) {
                const failedOp = {
                    ...op,
                    executionStatus: "failed",
                };
                setExecutingEditOperations((ops) => ops ? ops.filter((x) => x.id !== op.id) : []);
                return failedOp;
            }
            //await stateRef.current.refreshHistory(op.mainItem);
            //stateRef.current.addToEditHistory(result.data as EditOperation);
            const returnedOp = result.data;
            setEditOperationExecuted(returnedOp);
            keepRemovingIndicatorsUntilSocketCompletion =
                returnedOp.executionStatus === "executing";
            // Only remove from executing operations if it's not a long-running operation
            // still in progress. Long-running ops will be removed when WebSocket sends
            // operation:completed or operation:failed
            if (returnedOp.executionStatus !== "executing") {
                setExecutingEditOperations((ops) => ops ? ops.filter((x) => x.id !== op.id) : []);
            }
            else {
                // Swap in the server's "executing" version so consumers (e.g. the
                // operations indicator) can tell long-running ops apart from ops that
                // are merely awaiting their HTTP response
                setExecutingEditOperations((ops) => (ops ?? []).map((x) => (x.id === op.id ? returnedOp : x)));
            }
            return returnedOp;
        }
        finally {
            if (!keepRemovingIndicatorsUntilSocketCompletion) {
                clearRemovingIndicatorsForOperation(op.id);
            }
        }
    }, [clearRemovingIndicatorsForOperation, showRemovingIndicatorsForOperation]);
    const duplicateComponents = useCallback(({ componentIds }) => {
        const op = {
            type: "duplicate-components",
            mainItem: stateRef.current.page?.item.descriptor,
            sourceComponentIds: componentIds,
            date: new Date().toISOString(),
            id: uuid(),
            title: "Duplicate components",
        };
        executeOp(op);
    }, [executeOp]);
    const addComponent = useCallback(async (componentTypeId, placeholderKey, index, pageItem, options) => {
        const normalizedTypeId = (componentTypeId || "").trim();
        if (!normalizedTypeId ||
            normalizedTypeId.includes("[...]") ||
            !GUID_REGEX_EXACT.test(normalizedTypeId)) {
            return undefined;
        }
        // if (!page) {
        //   console.log("Add component: No page found");
        //   return;
        // }
        const addToSharedLayout = stateRef.current.layoutMode === "shared";
        const op = {
            type: "add-component",
            mainItem: getItemDescriptor(pageItem),
            // parent: parentId
            //   ? {
            //       id: parentId,
            //       language: page.item.language,
            //       version: page.item.version,
            //     }
            //   : undefined,
            placeholderKey,
            placeholderIndex: index,
            componentTypeId: normalizedTypeId,
            date: new Date().toISOString(),
            id: uuid(),
            title: "Add component",
            addToSharedLayout,
        };
        let keepIndicatorUntilSocketCompletion = false;
        const showIndicator = options?.showIndicator !== false;
        if (showIndicator) {
            stateRef.current.setInserting({
                placeholderKey,
                index,
                operationId: op.id,
                color: options?.color,
            });
        }
        try {
            const result = await executeOp(op);
            keepIndicatorUntilSocketCompletion =
                result.executionStatus === "executing";
            return result;
        }
        finally {
            if (showIndicator && !keepIndicatorUntilSocketCompletion) {
                clearInsertingIndicatorForOperation(op.id);
            }
        }
    }, [clearInsertingIndicatorForOperation, executeOp]);
    const updateRenderingParameters = useCallback(async (componentId, parameters) => {
        const pageItem = stateRef.current.page?.item?.descriptor;
        if (!pageItem) {
            return;
        }
        const op = {
            type: "update-rendering-parameters",
            componentId,
            parameters,
            mainItem: pageItem,
            date: new Date().toISOString(),
            id: uuid(),
            title: "Update rendering parameters",
        };
        const result = await executeOp(op, { refresh: "immediate" });
        if (result.executionStatus === "failed") {
            throw new Error("Failed to update personalization rules");
        }
    }, [executeOp]);
    const updateRenderingRules = useCallback(async (componentId, rules, targetPageItem) => {
        const pageItem = targetPageItem ?? stateRef.current.page?.item?.descriptor;
        if (!pageItem) {
            throw new Error("Cannot update personalization without a page item");
        }
        const op = {
            type: "update-rendering-rules",
            componentId,
            rulesJson: JSON.stringify(rules),
            mainItem: pageItem,
            date: new Date().toISOString(),
            id: uuid(),
            title: "Update personalization",
        };
        // waitForQuietPeriod debounces the iframe refresh so rapid auto-saves
        // (typing in a parameter input) don't make the page flicker on every keystroke.
        const result = await executeOp(op, { refresh: "waitForQuietPeriod" });
        if (result.executionStatus === "failed") {
            throw new Error("Failed to update personalization rules");
        }
    }, [executeOp]);
    const handleResult = (result, options) => {
        if (handleErrorResult(result, ui, stateRef.current)) {
            return false;
        }
        if (!result.data) {
            return false;
        }
        const executedOps = Array.isArray(result.data)
            ? result.data
            : [result.data];
        for (const executedOp of executedOps) {
            if (executedOp &&
                (executedOp.type === "move-component" ||
                    executedOp.type === "add-component" ||
                    executedOp.type === "link-component" ||
                    executedOp.type === "duplicate-components")) {
                if (executedOp.focus) {
                    const focusId = cleanId(executedOp.focus) ?? executedOp.focus;
                    // Without auto-scroll: this selection lands while the canvas still
                    // shows (and measures) the pre-operation DOM — for a move, the
                    // component's old position. Scrolling there yanks the page away
                    // from where the user dropped.
                    (stateRef.current.selectWithoutAutoScroll ??
                        stateRef.current.setSelection)([focusId]);
                    stateRef.current.setFocusFieldComponentId(focusId);
                    // Clear insert mode immediately after selecting the newly added component
                    if (executedOp.type === "add-component") {
                        stateRef.current.setInsertMode(false);
                    }
                }
            }
            ui.onOperationExecuted(executedOp);
        }
        // Trigger a page refresh when requested.
        // We rely on the caller to pass refresh !== "none" (e.g. sidebar field editors),
        // while inline editing intentionally uses refresh:"none" to avoid cursor jumps.
        if (options.refresh && options.refresh !== "none") {
            stateRef.current.requestRefresh(options.refresh === "immediate" ? "immediate" : "waitForQuietPeriod");
        }
        return true;
    };
    const ensureLock = useCallback(async (field) => {
        if (!field || !field.item) {
            return false;
        }
        const lockKey = getFieldLockKey(field);
        const locallyAcquiredLock = acquiredFieldLockRef.current;
        const isAlreadyLocked = fieldsMatchForLock(stateRef.current.lockedField, field) ||
            fieldsMatchForLock(locallyAcquiredLock, field);
        if (isAlreadyLocked) {
            return true;
        }
        const existingPendingLock = pendingFieldLocks.current.get(lockKey);
        if (existingPendingLock) {
            return existingPendingLock;
        }
        const pendingLock = (async () => {
            const abortController = new AbortController();
            const timeoutId = window.setTimeout(() => {
                abortController.abort();
            }, FIELD_LOCK_REQUEST_TIMEOUT_MS);
            let result;
            try {
                result = await lockField(field.item, field.fieldId, stateRef.current.sessionId, abortController.signal);
            }
            finally {
                window.clearTimeout(timeoutId);
            }
            if (result.type == "error") {
                if (abortController.signal.aborted) {
                    console.warn("field lock request timed out", {
                        fieldId: field.fieldId,
                        itemId: field.item.id,
                        timeoutMs: FIELD_LOCK_REQUEST_TIMEOUT_MS,
                    });
                    return false;
                }
                console.log("error locking field", result);
            }
            if (handleErrorResult(result, ui, stateRef.current))
                return false;
            if (result.type == "success" && result.data.success) {
                acquiredFieldLockRef.current = field;
                stateRef.current.setLockedField(field);
                return true;
            }
            return false;
        })();
        pendingFieldLocks.current.set(lockKey, pendingLock);
        try {
            return await pendingLock;
        }
        finally {
            if (pendingFieldLocks.current.get(lockKey) === pendingLock) {
                pendingFieldLocks.current.delete(lockKey);
            }
        }
    }, [ui]);
    const lockItemsAndRefresh = useCallback(async (items) => {
        const item = items[0];
        if (!item)
            return;
        await lockItems(items);
        if (item.id == stateRef.current.item?.id) {
            await stateRef.current.loadItem(item);
        }
        else
            stateRef.current.requestRefresh("immediate");
    }, []);
    const unlockItemsAndRefresh = useCallback(async (items) => {
        const item = items[0];
        if (!item)
            return;
        await unlockItems(items);
        if (item.id == stateRef.current.item?.id) {
            await stateRef.current.loadItem(item);
        }
        else
            stateRef.current.requestRefresh("immediate");
    }, []);
    const refreshWorkboxAfterWorkflowChange = useDebouncedCallback(() => {
        stateRef.current.refreshWorkbox?.();
    }, 300, { trailing: true });
    const executeWorkflowCommandAndRefresh = useCallback(async (item, commandId) => {
        const result = await executeWorkflowCommand(item, commandId, "");
        if (result.type === "success") {
            if (!result.data.succeeded) {
                const errorMessage = {
                    summary: "Workflow command failed",
                    details: result.data.message,
                };
                ui.showErrorToast(errorMessage);
            }
            if (result.data.succeeded) {
                refreshWorkboxAfterWorkflowChange();
            }
            stateRef.current.requestRefresh("immediate");
        }
    }, [refreshWorkboxAfterWorkflowChange, ui]);
    const renameItem = useCallback(async (item, newName) => {
        await executeOp({
            type: "rename-item",
            itemId: item.id,
            mainItem: stateRef.current.page?.item.descriptor || getItemDescriptor(item),
            date: new Date().toISOString(),
            id: uuid(),
            item: getItemDescriptor(item),
            newName,
            title: "Rename item",
        });
        stateRef.current.requestRefresh("immediate");
        // Only properties changed (name/path), not children.
        // Refresh both the operation descriptor and (when applicable) the currently loaded
        // descriptor to handle latest-version descriptor mismatches.
        const currentLoadedItem = stateRef.current.item;
        const descriptorsToRefresh = [item];
        if (currentLoadedItem &&
            currentLoadedItem.id === item.id &&
            currentLoadedItem.language === item.language) {
            descriptorsToRefresh.push(currentLoadedItem.descriptor);
        }
        // Await the refresh to ensure cache is updated and events are fired before returning.
        await itemsRepository.refreshItems(descriptorsToRefresh, {
            properties: true,
            children: false,
        });
        // If the renamed item is currently loaded, force a descriptor reload so
        // UI panels (e.g. Item Info) don't remain one rename behind.
        if (currentLoadedItem &&
            currentLoadedItem.id === item.id &&
            currentLoadedItem.language === item.language) {
            await stateRef.current.loadItem(currentLoadedItem.descriptor);
        }
    }, [itemsRepository, executeOp]);
    const lastEditField = useRef(undefined);
    const pendingFieldSaves = useRef(new Map());
    const fieldEditGeneration = useRef(0);
    const completedFieldSaveGeneration = useRef(0);
    const latestSavedFieldEditOperationId = useRef(undefined);
    const getFieldSaveKey = (field) => {
        const database = field.item.database || "master";
        return `${field.item.id}:${field.item.language}:${field.item.version}:${database}:${field.fieldId}`.toLowerCase();
    };
    const enqueueFieldSave = useCallback(async (field, save) => {
        const key = getFieldSaveKey(field);
        const previousSave = pendingFieldSaves.current.get(key);
        const queuedSave = (previousSave ?? Promise.resolve())
            .catch(() => undefined)
            .then(save);
        pendingFieldSaves.current.set(key, queuedSave);
        try {
            await queuedSave;
        }
        finally {
            if (pendingFieldSaves.current.get(key) === queuedSave) {
                pendingFieldSaves.current.delete(key);
            }
        }
    }, []);
    const resolveOriginatingFieldModificationStore = useCallback((originatingSlotId) => {
        if (originatingSlotId) {
            return stateRef.current.getFieldModificationStoreForSlot?.(originatingSlotId);
        }
        const activeSlotStore = stateRef.current.getActiveSlotContext?.()?.fieldModificationStore;
        if (activeSlotStore)
            return activeSlotStore;
        const activeSlotId = stateRef.current.getActiveSlotId?.();
        return activeSlotId
            ? stateRef.current.getFieldModificationStoreForSlot?.(activeSlotId)
            : undefined;
    }, []);
    const editField = useCallback(async ({ field, value, rawValue, forceMode, refresh = "immediate", originatingSlotId, }) => {
        const effectiveMode = forceMode ?? stateRef.current.mode;
        logFieldValueDiagnostic("operations", "editField requested", {
            field: summarizeFieldDescriptor(field),
            value: summarizeFieldValue(value),
            rawValue: summarizeFieldValue(rawValue),
            forceMode,
            effectiveMode,
            refresh,
            originatingSlotId,
            undoing: undoing.current,
        });
        fieldEditGeneration.current += 1;
        if (effectiveMode === "suggestions" &&
            (stateRef.current.page || stateRef.current.currentItemDescriptor)) {
            // Create a unique key for this field to track pending saves
            const fieldKey = getSuggestionFieldIdentityKey({
                itemId: field.item.id,
                language: field.item.language,
                version: field.item.version,
                fieldId: field.fieldId,
            });
            const pendingFieldKey = `${fieldKey}:${stateRef.current.user?.name || "unknown"}`;
            const op = await getOrMergeSuggestedEditOp(field, rawValue, value, {
                page: stateRef.current.page,
                currentItemDescriptor: stateRef.current.currentItemDescriptor,
                user: stateRef.current.user,
                itemsRepository: itemsRepository,
                comments: stateRef.current.comments,
                commentsLoaded: stateRef.current.commentsLoaded,
                suggestedEdits: stateRef.current.suggestedEdits,
                setSuggestedEdits: stateRef.current.setSuggestedEdits,
            });
            if (op) {
                logFieldValueDiagnostic("operations", "suggested edit prepared", {
                    field: summarizeFieldDescriptor(field),
                    fieldKey,
                    value: summarizeFieldValue(value),
                    rawValue: summarizeFieldValue(rawValue),
                    opId: op.id,
                    oldValue: summarizeFieldValue(op.oldValue),
                    newValue: summarizeFieldValue(op.newValue),
                });
                // Keep local suggestion state in step with typing immediately, then
                // serialize persistence behind any in-flight save for this field.
                // Create and track the save promise. useDebouncedCallback can return
                // undefined before the trailing call runs, so this path uses a real
                // promise that resolves after the delayed save completes.
                const saveSnapshot = { ...op };
                const expectedNewValue = saveSnapshot.newValue;
                const savePromise = enqueueSuggestedEditFieldTask(fieldKey, () => createDelayedSuggestedEditSave(saveSnapshot))
                    .then((result) => {
                    // Update the suggested edits state with the result from the server
                    if (result.type === "success" && result.data) {
                        const updatedEdit = result.data;
                        if (isSuggestedEditRemoved(updatedEdit.id)) {
                            // Reverted to a no-op while this save was in flight; make sure
                            // it stays gone on the server and do not re-add it locally.
                            void deleteSuggestedEdit(updatedEdit).then((deleteResult) => {
                                if (deleteResult.type !== "success") {
                                    console.warn("Failed to delete reverted suggestion after save:", deleteResult.summary, deleteResult.details);
                                }
                            }, () => undefined);
                            return result;
                        }
                        stateRef.current.setSuggestedEdits((current) => upsertSuggestedEdit(current, updatedEdit, {
                            expectedNewValue,
                            source: "save-ack",
                        }));
                    }
                    else if (result.type === "conflict") {
                        if (result.suggestion) {
                            stateRef.current.setSuggestedEdits((current) => upsertSuggestedEdit(current, result.suggestion, {
                                source: "save-ack",
                            }));
                            if (result.suggestion.status !== "pending" &&
                                pendingSuggestedEdits.get(pendingFieldKey)?.id ===
                                    result.suggestion.id) {
                                pendingSuggestedEdits.delete(pendingFieldKey);
                            }
                        }
                    }
                    else if (result.type === "error" ||
                        result.type === "unauthorized") {
                        // Show error toast for failed suggested edit save — unless the
                        // suggestion was reverted while the request was in flight, in
                        // which case there is nothing for the user to see or retry.
                        if (!isSuggestedEditRemoved(saveSnapshot.id)) {
                            ui.showErrorToast({
                                summary: result.summary || "Failed to save suggestion",
                                details: result.details,
                                rawDetails: result.rawDetails,
                            });
                        }
                    }
                    return result;
                })
                    .finally(() => {
                    // Also remove from pending edits once this exact value is
                    // persisted. If the user typed again, keep the newer draft.
                    const pendingEdit = pendingSuggestedEdits.get(pendingFieldKey);
                    if (pendingEdit?.id === saveSnapshot.id &&
                        pendingEdit.newValue === saveSnapshot.newValue) {
                        pendingSuggestedEdits.delete(pendingFieldKey);
                    }
                });
                await savePromise;
            }
            logFieldValueDiagnostic("operations", "suggestions edit complete", {
                field: summarizeFieldDescriptor(field),
                fieldKey,
                hadOperation: !!op,
            });
            return;
        }
        const fieldModificationStore = resolveOriginatingFieldModificationStore(originatingSlotId);
        const saveSequence = await itemsRepository.updateFieldValue(field, stateRef.current.user ?? {
            name: "unknown",
            ai: false,
            agentId: undefined,
        }, true, value, rawValue, fieldModificationStore);
        logFieldValueDiagnostic("operations", "local field value updated", {
            field: summarizeFieldDescriptor(field),
            value: summarizeFieldValue(value),
            rawValue: summarizeFieldValue(rawValue),
            saveSequence,
            refresh,
            originatingSlotId,
            hasFieldModificationStore: !!fieldModificationStore,
        });
        if (refresh === "immediate") {
            logFieldValueDiagnostic("operations", "saving immediately", {
                field: summarizeFieldDescriptor(field),
                saveSequence,
            });
            return enqueueFieldSave(field, () => editFieldImmediate({
                field,
                value,
                rawValue,
                refresh,
                saveSequence,
                fieldModificationStore,
            }));
        }
        const isNewField = lastEditField.current?.fieldId !== field.fieldId ||
            lastEditField.current?.item?.id !== field.item?.id ||
            lastEditField.current?.item?.language !== field.item?.language ||
            lastEditField.current?.item?.version !== field.item?.version;
        if (isNewField) {
            logFieldValueDiagnostic("operations", "flushing previous field debounce", {
                previousField: summarizeFieldDescriptor(lastEditField.current),
                nextField: summarizeFieldDescriptor(field),
            });
            executeEditFieldDebounced.flush();
        }
        lastEditField.current = field;
        // Fix: Pass the saveSequence to avoid calling updateFieldValue twice
        logFieldValueDiagnostic("operations", "queueing debounced save", {
            field: summarizeFieldDescriptor(field),
            value: summarizeFieldValue(value),
            rawValue: summarizeFieldValue(rawValue),
            refresh,
            saveSequence,
        });
        return executeEditFieldDebounced({
            field,
            value,
            rawValue,
            refresh,
            saveSequence,
            fieldModificationStore,
        });
    }, [
        itemsRepository,
        getOrMergeSuggestedEditOp,
        resolveOriginatingFieldModificationStore,
        enqueueFieldSave,
    ]);
    const editFieldImmediate = useCallback(async ({ field, value, rawValue, refresh = "immediate", saveSequence, fieldModificationStore, }) => {
        const val = rawValue !== undefined ? rawValue : value;
        if (val === undefined)
            return;
        logFieldValueDiagnostic("operations", "editFieldImmediate start", {
            field: summarizeFieldDescriptor(field),
            value: summarizeFieldValue(value),
            rawValue: summarizeFieldValue(rawValue),
            persistedValue: summarizeFieldValue(val),
            refresh,
            saveSequence,
            hasFieldModificationStore: !!fieldModificationStore,
        });
        const op = await getEditOP(field, rawValue, value);
        // Fix: Handle getEditOP returning undefined (item/field not found)
        // Clear dirty state so the field doesn't get stuck in "saving" state
        if (!op) {
            console.warn("[editFieldImmediate] Could not create edit operation for field:", field.fieldId);
            logFieldValueDiagnostic("operations", "editFieldImmediate no op", {
                field: summarizeFieldDescriptor(field),
                value: summarizeFieldValue(value),
                rawValue: summarizeFieldValue(rawValue),
                saveSequence,
            });
            itemsRepository.clearDirtyState(field, fieldModificationStore);
            return;
        }
        try {
            const result = await executeOp(op, { refresh });
            logFieldValueDiagnostic("operations", "editFieldImmediate result", {
                field: summarizeFieldDescriptor(field),
                opId: op.id,
                executionStatus: result?.executionStatus,
                refresh,
                saveSequence,
                persistedValue: summarizeFieldValue(val),
            });
            // Fix: Check if the operation actually succeeded
            // If result is undefined/null, the operation likely failed
            if (!result) {
                console.warn("[editFieldImmediate] Edit operation returned no result, clearing dirty state");
                logFieldValueDiagnostic("operations", "editFieldImmediate no result", {
                    field: summarizeFieldDescriptor(field),
                    opId: op.id,
                    saveSequence,
                });
                itemsRepository.clearDirtyState(field, fieldModificationStore);
                return;
            }
            // Don't refresh here - the websocket handler will trigger a refresh when
            // it receives the item-changed notification from the server. This avoids
            // duplicate refresh calls for the same edit.
            if (result.executionStatus !== "failed" &&
                isWorkflowMetadataField(op.fieldId, op.fieldName)) {
                refreshWorkboxAfterWorkflowChange();
            }
            if (result.executionStatus !== "failed") {
                latestSavedFieldEditOperationId.current = op.id;
                completedFieldSaveGeneration.current += 1;
            }
            itemsRepository.onFieldSaved(field, val, saveSequence || 0, fieldModificationStore);
        }
        catch (error) {
            // Fix: Handle network errors and other exceptions
            // Clear the dirty state to prevent the field from getting stuck in "saving" state
            console.error("[editFieldImmediate] Error saving field:", error);
            logFieldValueDiagnostic("operations", "editFieldImmediate failed", {
                field: summarizeFieldDescriptor(field),
                value: summarizeFieldValue(value),
                rawValue: summarizeFieldValue(rawValue),
                saveSequence,
                error: error instanceof Error ? error.message : String(error),
            });
            itemsRepository.clearDirtyState(field, fieldModificationStore);
        }
    }, [itemsRepository, executeOp, refreshWorkboxAfterWorkflowChange]);
    const executeEditFieldDebounced = useDebouncedCallback(async ({ field, value, rawValue, refresh, saveSequence, fieldModificationStore, }) => {
        // Fix: Use the saveSequence passed from editField instead of calling
        // updateFieldValue again. This prevents the race condition where the
        // field gets marked dirty twice with different sequences.
        logFieldValueDiagnostic("operations", "debounced save executing", {
            field: summarizeFieldDescriptor(field),
            value: summarizeFieldValue(value),
            rawValue: summarizeFieldValue(rawValue),
            refresh,
            saveSequence,
            hasFieldModificationStore: !!fieldModificationStore,
        });
        return enqueueFieldSave(field, () => editFieldImmediate({
            field,
            value,
            rawValue,
            refresh,
            saveSequence,
            fieldModificationStore,
        }));
    }, stateRef.current.configuration.debounceFieldEditsInterval * 2, { trailing: true });
    const flushPendingFieldEdits = useCallback(async () => {
        const completedGenerationBeforeFlush = completedFieldSaveGeneration.current;
        const hadPendingDebouncedEdit = executeEditFieldDebounced.isPending();
        if (hadPendingDebouncedEdit) {
            await executeEditFieldDebounced.flush();
        }
        const inFlightSaves = Array.from(pendingFieldSaves.current.values());
        if (inFlightSaves.length > 0) {
            await Promise.allSettled(inFlightSaves);
        }
        return completedFieldSaveGeneration.current > completedGenerationBeforeFlush
            ? latestSavedFieldEditOperationId.current
            : undefined;
    }, [executeEditFieldDebounced]);
    // Note: We intentionally keep a single save debounce window here.
    async function getEditOP(field, rawValue, value) {
        const item = await itemsRepository.getItem(field.item);
        if (!item)
            return;
        // Check the local field items cache first - field items (template field definitions)
        // are static and don't change during an editing session
        let fieldItem = fieldItemsCache.current.get(field.fieldId);
        if (!fieldItem) {
            fieldItem = await itemsRepository.getItem({
                id: field.fieldId,
                language: "en",
                version: 0,
            });
            if (fieldItem) {
                fieldItemsCache.current.set(field.fieldId, fieldItem);
            }
        }
        if (!fieldItem) {
            console.log("Could not fetch field item " + field.fieldId);
            return;
        }
        let op = lastOp.current;
        if (!op ||
            op.undone ||
            op.type !== "edit-field" ||
            !op.itemId ||
            op.itemId !== item.id ||
            op.fieldId !== field.fieldId ||
            op.mainItem.language !== item.language ||
            op.mainItem.version !== item.version) {
            // Use state.page's descriptor only if its language AND version match the field's
            // to prevent race conditions when switching languages or versions quickly
            const page = stateRef.current.page;
            const pageMatchesField = page?.item.language === item.language &&
                page?.item.version === item.version;
            const mainItemDescriptor = pageMatchesField && page ? page.item.descriptor : item.descriptor;
            op = {
                type: "edit-field",
                itemId: item.id,
                fieldId: field.fieldId,
                fieldName: fieldItem.name,
                date: new Date().toISOString(),
                id: uuid(),
                title: "Edit " + item.name,
                focus: item.id + ":" + field.fieldId,
                canUndo: true,
                mainItem: mainItemDescriptor,
                agentId: stateRef.current.user?.agentId,
            };
        }
        op.date = new Date().toISOString();
        op.value = rawValue ?? value;
        // Preserve agentId from user context if available
        if (stateRef.current.user?.agentId) {
            op.agentId = stateRef.current.user.agentId;
        }
        const firstEditInheritedComponent = item.isInheritedFromMasterLanguage &&
            op.mainItem.language != stateRef.current.page?.item.language;
        if (firstEditInheritedComponent)
            op.mainItem.language = stateRef.current.page.item.language;
        lastOp.current = op;
        return op;
    }
    const executeUndoOperation = useCallback(async (ops) => {
        const result = await executeUndo(stateRef.current.sessionId, ops.map((x) => x.id));
        // Update state only AFTER API call completes - no optimistic update
        // This ensures the UI only changes when the operation truly completes
        const operationWasRemoved = isMissingEditOperationResult(result);
        if (!operationWasRemoved) {
            handleResult(result, { refresh: "immediate" });
        }
        if (result.type === "success") {
            for (const executedOp of result.data) {
                recentlyUndoneIds.current.add(executedOp.id);
                recentlyRedoneIds.current.delete(executedOp.id);
            }
            // Check if any operations are executing in background (long-running undo)
            const executingOps = result.data.filter((x) => x.executionStatus === "executing");
            if (executingOps.length > 0) {
                // Add to executing operations list - progress will be tracked via WebSocket
                setExecutingEditOperations((prev) => [...prev, ...executingOps]);
                // Return true - the operation is in progress, success will be notified via WebSocket
                return true;
            }
            // All undo operations require a full page refresh to update the UI properly.
            // - undoing add-component: removes the added component from the page
            // - undoing remove-component: restores the deleted component to the page
            // - undoing delete-items: restores deleted items
            // - undoing edit-field: reverts field changes
            // The targeted refresh via itemsRepository.refreshItems is not sufficient because:
            // 1. When undoing add-component, the component no longer exists to refresh
            // 2. When undoing remove-component, the component wasn't in cache
            const hasDeleteOperations = true; // Always refresh after undo
            const itemsToRefresh = result.data
                .map((x) => {
                if (x.type === "edit-field")
                    return {
                        ...x.mainItem,
                        id: x.itemId,
                    };
                if (x.type === "add-component")
                    return {
                        id: x.componentId,
                        language: x.mainItem.language,
                        version: x.mainItem.version,
                    };
                return undefined;
            })
                .flat()
                .filter((x) => x != null);
            await itemsRepository.refreshItems(itemsToRefresh);
            // handleResult already triggers the single immediate page refresh.
            // Avoid a second request here, which can flood a busy rendering host.
            return true;
        }
        else {
            // Refresh history so toolbar state matches server (fixes inconsistent UI under load)
            stateRef.current.refreshHistory?.(stateRef.current.historyMode, stateRef.current.showOnlyMyChanges);
            return false;
        }
    }, [ui, itemsRepository]);
    const undoOperations = useCallback(async (operationIds) => {
        if (!operationIds.length)
            return false;
        if (undoing.current) {
            const deadline = Date.now() + 5000;
            while (undoing.current && Date.now() < deadline) {
                await new Promise((r) => setTimeout(r, 50));
            }
            if (undoing.current)
                return false;
        }
        undoing.current = true;
        try {
            // Map operation IDs to full objects from history, or stubs if not found
            const opsToUndo = operationIds.map((id) => {
                const found = stateRef.current.editHistory.find((o) => o.id === id);
                if (found)
                    return found;
                // Return a stub if not found in local history
                return { id, type: "unknown", undone: false };
            });
            // Sort by date descending (newest first) if we have the dates
            const sortedOps = [...opsToUndo].sort((a, b) => {
                if (!a.date || !b.date)
                    return 0;
                return new Date(b.date).getTime() - new Date(a.date).getTime();
            });
            return await executeUndoOperation(sortedOps);
        }
        finally {
            undoing.current = false;
        }
    }, [executeUndoOperation]);
    const undo = useCallback(async (operationId) => {
        if (undoing.current) {
            const deadline = Date.now() + 5000;
            while (undoing.current && Date.now() < deadline) {
                await new Promise((r) => setTimeout(r, 50));
            }
            if (undoing.current)
                return false;
        }
        undoing.current = true;
        try {
            const pendingFieldOperationId = await flushPendingFieldEdits();
            if (pendingFieldOperationId) {
                operationId = pendingFieldOperationId;
            }
            const currentItem = stateRef.current.item?.descriptor;
            const filterBySession = stateRef.current.showOnlyMyChanges;
            const historyMode = stateRef.current.historyMode;
            let ops;
            // If the caller supplied an operationId that was already undone in this session
            // (React hasn't re-rendered yet, so the UI may hold a stale reference), discard it
            // and fall through to the general selection logic below.
            if (operationId && recentlyUndoneIds.current.has(operationId)) {
                operationId = undefined;
            }
            // If a specific operationId is provided (from the UI), always honor it so the UI and
            // server stay in sync under load. Use the op from history if present (for confirmations),
            // otherwise call the API with just the id (server is source of truth).
            if (operationId) {
                const targetOp = stateRef.current.editHistory.find((x) => x.id === operationId && x.canUndo);
                if (targetOp) {
                    ops = [targetOp];
                }
                else {
                    // Not in local state (e.g. history refreshed under load). Call API with this id
                    // so we don't silently do nothing; server will accept or return an error.
                    ops = [
                        { id: operationId, type: "unknown" },
                    ];
                }
            }
            else {
                // Select undoable operations respecting showOnlyMyChanges and historyMode filters
                ops = stateRef.current.editHistory
                    .filter((x) => {
                    if (!x.canUndo)
                        return false;
                    if (recentlyUndoneIds.current.has(x.id))
                        return false;
                    // Session filter (showOnlyMyChanges)
                    if (filterBySession &&
                        x.sessionId !== stateRef.current.sessionId) {
                        return false;
                    }
                    // Page-centric filter: only include operations for current page and language (any version)
                    if (historyMode === "page-centric" && currentItem) {
                        if (!x.mainItem ||
                            x.mainItem.id !== currentItem.id ||
                            x.mainItem.language !== currentItem.language) {
                            return false;
                        }
                    }
                    // Current-version filter: only include operations for current page, language, and version
                    if (historyMode === "current-version" && currentItem) {
                        if (!x.mainItem ||
                            x.mainItem.id !== currentItem.id ||
                            x.mainItem.language !== currentItem.language ||
                            x.mainItem.version !== currentItem.version) {
                            return false;
                        }
                    }
                    return true;
                })
                    .slice(0, 1);
            }
            if (!ops.length) {
                undoing.current = false;
                return false;
            }
            const firstOp = ops[0];
            if (!firstOp) {
                undoing.current = false;
                return false;
            }
            // When we only have an id (op not in local history), skip confirmations and call API
            if (firstOp.type === "unknown") {
                return await executeUndoOperation(ops);
            }
            // Check if this is a cross-session operation and show warning first
            const isCrossSession = firstOp.sessionId !== stateRef.current.sessionId;
            if (isCrossSession) {
                const confirmed = await new Promise((resolve) => {
                    ui.confirmationDialogRef.current?.confirm({
                        header: "Undo operation from another user / session",
                        message: React.createElement("div", { className: "space-y-3" }, React.createElement("p", null, `This will undo an operation created by ${firstOp.user?.name || "another user"} in another session. Are you sure you want to proceed?`), React.createElement(TreeOperationConfirmContent, {
                            title: firstOp.title,
                            description: firstOp.description,
                        })),
                        acceptLabel: "Undo",
                        rejectLabel: "Cancel",
                        showCancel: false,
                        accept: () => resolve(true),
                        reject: () => resolve(false),
                    });
                });
                if (!confirmed) {
                    undoing.current = false;
                    return false;
                }
            }
            // Check if this is a tree-modifying operation that requires confirmation
            // Skip if already confirmed as cross-session (first confirmation is sufficient)
            if (firstOp && isTreeModifyingOperation(firstOp) && !isCrossSession) {
                return new Promise((resolve) => {
                    const operationTypeName = firstOp.type === "create-item"
                        ? "Create Item"
                        : firstOp.type === "delete-items"
                            ? "Delete Items"
                            : firstOp.type === "move-items"
                                ? "Move Items"
                                : firstOp.type === "copy-items"
                                    ? "Copy Items"
                                    : firstOp.type === "clone-items"
                                        ? "Clone Items"
                                        : firstOp.type;
                    ui.confirmationDialogRef.current?.confirm({
                        header: `Undo ${operationTypeName}?`,
                        message: React.createElement(TreeOperationConfirmContent, {
                            title: firstOp.title,
                            description: firstOp.description,
                        }),
                        acceptLabel: "Undo",
                        rejectLabel: "Cancel",
                        showCancel: false,
                        accept: async () => {
                            // After user confirms, proceed with external changes check
                            const impactResult = await checkOperationImpact(stateRef.current.sessionId, firstOp.id, "undo");
                            if (impactResult.type === "success" &&
                                impactResult.data?.hasExternalChanges) {
                                // Show warning dialog about external changes
                                const impact = impactResult.data;
                                ui.confirmationDialogRef.current?.confirm({
                                    header: "Warning: This undo will affect other users' work",
                                    message: React.createElement(ExternalChangesWarningContent, {
                                        impact,
                                    }),
                                    acceptLabel: "Undo anyway",
                                    rejectLabel: "Cancel",
                                    showCancel: false,
                                    accept: async () => {
                                        const result = await executeUndoOperation(ops);
                                        undoing.current = false;
                                        resolve(result);
                                    },
                                    reject: () => {
                                        undoing.current = false;
                                        resolve(false);
                                    },
                                });
                            }
                            else {
                                // No external changes, proceed with undo
                                const result = await executeUndoOperation(ops);
                                undoing.current = false;
                                resolve(result);
                            }
                        },
                        reject: () => {
                            undoing.current = false;
                            resolve(false);
                        },
                    });
                });
            }
            // Check for external changes that would be affected by this undo
            const impactResult = await checkOperationImpact(stateRef.current.sessionId, firstOp.id, "undo");
            if (impactResult.type === "success" &&
                impactResult.data?.hasExternalChanges) {
                // Show warning dialog about external changes
                return new Promise((resolve) => {
                    const impact = impactResult.data;
                    ui.confirmationDialogRef.current?.confirm({
                        header: "Warning: This undo will affect other users' work",
                        message: React.createElement(ExternalChangesWarningContent, {
                            impact,
                        }),
                        acceptLabel: "Undo anyway",
                        rejectLabel: "Cancel",
                        showCancel: false,
                        accept: async () => {
                            const result = await executeUndoOperation(ops);
                            undoing.current = false;
                            resolve(result);
                        },
                        reject: () => {
                            undoing.current = false;
                            resolve(false);
                        },
                    });
                });
            }
            // Check if the operation's mainItem matches the currently loaded item
            if (firstOp &&
                firstOp.mainItem &&
                currentItem &&
                (firstOp.mainItem.id !== currentItem.id ||
                    firstOp.mainItem.language !== currentItem.language ||
                    firstOp.mainItem.version !== currentItem.version)) {
                // Operation is on a different item - ask user if they want to load it first
                return new Promise((resolve) => {
                    ui.confirmationDialogRef.current?.confirm({
                        header: "Undo operation on different item",
                        message: React.createElement(UndoDifferentItemContent, {
                            operationTitle: firstOp.title,
                            itemId: firstOp.mainItem.id,
                            itemLanguage: firstOp.mainItem.language,
                        }),
                        acceptLabel: "Load item first",
                        rejectLabel: "Undo anyway",
                        showCancel: true,
                        accept: async () => {
                            // Load the item first, then user can manually trigger undo
                            await stateRef.current.loadItem(firstOp.mainItem);
                            undoing.current = false;
                            resolve(false); // Return false to indicate undo wasn't executed yet
                        },
                        reject: async () => {
                            // Proceed with undo without loading
                            const result = await executeUndoOperation(ops);
                            undoing.current = false;
                            resolve(result);
                        },
                    });
                });
            }
            // Operation is on current item or no item loaded - proceed normally
            return await executeUndoOperation(ops);
        }
        finally {
            // Always reset the flag
            undoing.current = false;
            console.log("undoing flag reset");
        }
    }, [ui, executeUndoOperation, flushPendingFieldEdits]);
    const executeRedoOperation = useCallback(async (op) => {
        const result = await executeRedo(stateRef.current.sessionId, op.id);
        // NOTE: We no longer update editHistory here. The single source of truth is EditorShell,
        // which merges server responses + websocket updates while enforcing scope filtering.
        const operationWasRemoved = isMissingEditOperationResult(result);
        if (!operationWasRemoved) {
            handleResult(result, { refresh: "immediate" });
        }
        if (result.type === "success") {
            for (const executedOp of result.data) {
                recentlyRedoneIds.current.add(executedOp.id);
                recentlyUndoneIds.current.delete(executedOp.id);
            }
            // Check if the operation is executing in background (long-running redo)
            const executingOps = result.data.filter((x) => x.executionStatus === "executing");
            if (executingOps.length > 0) {
                // Add to executing operations list - progress will be tracked via WebSocket
                setExecutingEditOperations((prev) => [...prev, ...executingOps]);
                // Return true - the operation is in progress, success will be notified via WebSocket
                return true;
            }
            const itemsToRefresh = result.data
                .map((x) => {
                if (x.type === "edit-field")
                    return {
                        ...x.mainItem,
                        id: x.itemId,
                    };
                if (x.type === "add-component")
                    return {
                        id: x.componentId,
                        language: x.mainItem.language,
                        version: x.mainItem.version,
                    };
                return undefined;
            })
                .filter((x) => x != null);
            await itemsRepository.refreshItems(itemsToRefresh);
            return true;
        }
        else {
            stateRef.current.refreshHistory?.(stateRef.current.historyMode, stateRef.current.showOnlyMyChanges);
            return false;
        }
    }, [ui, itemsRepository]);
    const redo = useCallback(async (operationId) => {
        if (undoing.current) {
            const deadline = Date.now() + 5000;
            while (undoing.current && Date.now() < deadline) {
                await new Promise((r) => setTimeout(r, 50));
            }
            if (undoing.current)
                return false;
        }
        undoing.current = true;
        try {
            await flushPendingFieldEdits();
            const currentItem = stateRef.current.item?.descriptor;
            if (!currentItem)
                return false;
            const filterBySession = stateRef.current.showOnlyMyChanges;
            const historyMode = stateRef.current.historyMode;
            let op;
            if (operationId && recentlyRedoneIds.current.has(operationId)) {
                operationId = undefined;
            }
            // If a specific operationId is provided (from the UI), always honor it so the UI and
            // server stay in sync under load. Use the op from history if present (for confirmations),
            // otherwise call the API with just the id (server is source of truth).
            if (operationId) {
                op = stateRef.current.editHistory.find((x) => x.id === operationId && x.canRedo);
                if (!op) {
                    op = {
                        id: operationId,
                        type: "unknown",
                    };
                }
            }
            else {
                // Select redoable operations respecting showOnlyMyChanges and historyMode filters
                op = [...stateRef.current.editHistory].reverse().find((x) => {
                    if (!x.canRedo)
                        return false;
                    if (recentlyRedoneIds.current.has(x.id))
                        return false;
                    // Session filter (showOnlyMyChanges)
                    if (filterBySession && x.sessionId !== stateRef.current.sessionId) {
                        return false;
                    }
                    // Page-centric filter: only include operations for current page and language (any version)
                    if (historyMode === "page-centric" && currentItem) {
                        if (!x.mainItem ||
                            x.mainItem.id !== currentItem.id ||
                            x.mainItem.language !== currentItem.language) {
                            return false;
                        }
                    }
                    // Current-version filter: only include operations for current page, language, and version
                    if (historyMode === "current-version" && currentItem) {
                        if (!x.mainItem ||
                            x.mainItem.id !== currentItem.id ||
                            x.mainItem.language !== currentItem.language ||
                            x.mainItem.version !== currentItem.version) {
                            return false;
                        }
                    }
                    return true;
                });
            }
            if (!op)
                return false;
            // When we only have an id (op not in local history), skip confirmations and call API
            if (op.type === "unknown") {
                return await executeRedoOperation(op);
            }
            // Check if this is a cross-session operation and show warning first
            const isCrossSession = op.sessionId !== stateRef.current.sessionId;
            if (isCrossSession) {
                const confirmed = await new Promise((resolve) => {
                    ui.confirmationDialogRef.current?.confirm({
                        header: "Redo operation from another user",
                        message: `This will redo an operation created by ${op.user?.name || "another user"}. Are you sure you want to proceed?`,
                        acceptLabel: "Redo",
                        rejectLabel: "Cancel",
                        showCancel: false,
                        accept: () => resolve(true),
                        reject: () => resolve(false),
                    });
                });
                if (!confirmed) {
                    undoing.current = false;
                    return false;
                }
            }
            // Check if this is a tree-modifying operation that requires confirmation
            if (isTreeModifyingOperation(op)) {
                return new Promise((resolve) => {
                    const operationTypeName = op.type === "create-item"
                        ? "Create Item"
                        : op.type === "delete-items"
                            ? "Delete Items"
                            : op.type === "move-items"
                                ? "Move Items"
                                : op.type === "copy-items"
                                    ? "Copy Items"
                                    : op.type === "clone-items"
                                        ? "Clone Items"
                                        : op.type;
                    ui.confirmationDialogRef.current?.confirm({
                        header: `Redo ${operationTypeName}?`,
                        message: React.createElement(TreeOperationConfirmContent, {
                            title: op.title,
                            description: op.description,
                        }),
                        acceptLabel: "Redo",
                        rejectLabel: "Cancel",
                        showCancel: false,
                        accept: async () => {
                            // After user confirms, proceed with external changes check
                            const impactResult = await checkOperationImpact(stateRef.current.sessionId, op.id, "redo");
                            if (impactResult.type === "success" &&
                                impactResult.data?.hasExternalChanges) {
                                // Show warning dialog about external changes
                                const impact = impactResult.data;
                                ui.confirmationDialogRef.current?.confirm({
                                    header: "Warning: This redo will affect other users' work",
                                    message: React.createElement(ExternalChangesWarningContent, {
                                        impact,
                                    }),
                                    acceptLabel: "Redo anyway",
                                    rejectLabel: "Cancel",
                                    showCancel: false,
                                    accept: async () => {
                                        const success = await executeRedoOperation(op);
                                        undoing.current = false;
                                        resolve(success);
                                    },
                                    reject: () => {
                                        undoing.current = false;
                                        resolve(false);
                                    },
                                });
                            }
                            else {
                                // No external changes, proceed with redo
                                const success = await executeRedoOperation(op);
                                undoing.current = false;
                                resolve(success);
                            }
                        },
                        reject: () => {
                            undoing.current = false;
                            resolve(false);
                        },
                    });
                });
            }
            // Check for external changes that would be affected by this redo
            const impactResult = await checkOperationImpact(stateRef.current.sessionId, op.id, "redo");
            if (impactResult.type === "success" &&
                impactResult.data?.hasExternalChanges) {
                // Show warning dialog about external changes
                return new Promise((resolve) => {
                    const impact = impactResult.data;
                    ui.confirmationDialogRef.current?.confirm({
                        header: "Warning: This redo will affect other users' work",
                        message: React.createElement(ExternalChangesWarningContent, {
                            impact,
                        }),
                        acceptLabel: "Redo anyway",
                        rejectLabel: "Cancel",
                        showCancel: false,
                        accept: async () => {
                            const success = await executeRedoOperation(op);
                            undoing.current = false;
                            resolve(success);
                        },
                        reject: () => {
                            undoing.current = false;
                            resolve(false);
                        },
                    });
                });
            }
            return await executeRedoOperation(op);
        }
        finally {
            // Always reset the flag
            undoing.current = false;
        }
    }, [ui, itemsRepository, executeRedoOperation, flushPendingFieldEdits]);
    const deleteItems = useCallback(async (items, options) => {
        if (items.length === 0)
            return;
        const itemsToDelete = await itemsRepository.getItems(items);
        const itemNames = itemsToDelete.map((x) => x.name).join(", ");
        // Check for references before showing confirmation
        let references = [];
        try {
            const result = await checkItemReferences(items);
            if (result.type === "success" && result.data) {
                references = result.data;
            }
        }
        catch (error) {
            console.error("Failed to check item references:", error);
            // Continue with delete even if reference check fails
        }
        const hasReferences = references.some((ref) => ref.referencingItems.length > 0);
        // Show confirmation dialog (with reference warning if applicable)
        const result = await new Promise((resolve) => {
            const undoCheckboxId = `delete-enable-undo-${Date.now()}`;
            const hardDeleteCheckboxId = `delete-hard-delete-${Date.now()}`;
            const deleteOptions = (_jsxs("div", { className: "mt-4 space-y-3", children: [ENABLE_HARD_DELETE_IN_DELETE_DIALOG ? (_jsx("div", { className: "border-feedback-red bg-feedback-red-light/60 rounded-md border p-3", children: _jsxs("label", { className: "flex cursor-pointer items-start gap-2", children: [_jsx("input", { type: "checkbox", id: hardDeleteCheckboxId, "data-testid": "delete-hard-delete-checkbox", defaultChecked: options?.hardDelete ?? false, className: "border-border-default mt-0.5 h-4 w-4 rounded" }), _jsxs("span", { className: "space-y-1", children: [_jsx("span", { className: "text-feedback-red block text-sm font-medium", children: "Permanently delete (bypass recycle bin)" }), _jsx("span", { className: "text-feedback-red block text-xs", children: "This skips the recycle bin. Items can only be restored if undo is enabled for this operation." })] })] }) })) : null, _jsxs("label", { className: "flex cursor-pointer items-center gap-2", children: [_jsx("input", { type: "checkbox", id: undoCheckboxId, "data-testid": "delete-enable-undo-checkbox", defaultChecked: options?.canUndo ?? true, className: "border-border-default h-4 w-4 rounded" }), _jsx("span", { className: "text-sm", children: "Enable undo (serializes complete content)" })] })] }));
            const getEnableUndo = () => {
                const checkbox = document.getElementById(undoCheckboxId);
                return checkbox?.checked ?? options?.canUndo ?? true;
            };
            const getHardDelete = () => {
                if (!ENABLE_HARD_DELETE_IN_DELETE_DIALOG)
                    return false;
                const checkbox = document.getElementById(hardDeleteCheckboxId);
                return checkbox?.checked ?? options?.hardDelete ?? false;
            };
            if (hasReferences) {
                ui.confirmationDialogRef.current?.confirm({
                    header: "Delete Items with References",
                    message: (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { children: "You are about to delete the following items which are referenced by other content:" }), _jsx(DeleteItemsWarning, { references: references }), _jsx("p", { className: "text-muted-foreground mt-4 text-sm", children: "Deleting these items may break links in your content. Are you sure you want to proceed?" }), deleteOptions] })),
                    acceptLabel: "Delete Anyway",
                    rejectLabel: "Cancel",
                    accept: () => resolve({
                        confirmed: true,
                        enableUndo: getEnableUndo(),
                        hardDelete: getHardDelete(),
                    }),
                    reject: () => resolve({
                        confirmed: false,
                        enableUndo: options?.canUndo ?? true,
                        hardDelete: ENABLE_HARD_DELETE_IN_DELETE_DIALOG
                            ? (options?.hardDelete ?? false)
                            : false,
                    }),
                    showCancel: false,
                });
            }
            else {
                // No references - use simple confirmation
                ui.confirmationDialogRef.current?.confirm({
                    header: "Delete Items?",
                    message: (_jsxs("div", { children: [_jsxs("p", { children: ["Are you sure you want to delete", " ", _jsx("span", { className: "font-medium", children: itemNames }), "?"] }), deleteOptions] })),
                    acceptLabel: "Yes, delete",
                    rejectLabel: "Cancel",
                    accept: () => resolve({
                        confirmed: true,
                        enableUndo: getEnableUndo(),
                        hardDelete: getHardDelete(),
                    }),
                    reject: () => resolve({
                        confirmed: false,
                        enableUndo: options?.canUndo ?? true,
                        hardDelete: ENABLE_HARD_DELETE_IN_DELETE_DIALOG
                            ? (options?.hardDelete ?? false)
                            : false,
                    }),
                    showCancel: false,
                });
            }
        });
        if (!result.confirmed)
            return;
        const canUndoValue = result.enableUndo;
        const hardDeleteValue = result.hardDelete;
        const op = {
            type: "delete-items",
            items: items.map(getItemDescriptor),
            mainItem: stateRef.current.page?.item.descriptor ||
                getItemDescriptor(items[0]),
            date: new Date().toISOString(),
            id: uuid(),
            title: `${hardDeleteValue ? "Hard delete" : "Delete"} ${itemNames}`,
            canUndo: canUndoValue,
            hardDelete: hardDeleteValue,
        };
        const returnedOp = await executeOp(op);
        itemsRepository.onItemsDeleted(itemsToDelete.map((x) => ({
            item: x.descriptor,
            parentId: x.parentId,
        })));
        stateRef.current.requestRefresh("immediate");
        return true;
    }, [itemsRepository, executeOp, ui]);
    const moveItems = useCallback(async (items, target, index) => {
        // Use generic edit endpoint for long-running operation support
        const op = {
            type: "move-items",
            itemIds: items.map((i) => i.id),
            targetParentId: target.id,
            targetIndex: index >= 0 ? index : undefined,
            mainItem: getItemDescriptor(target),
            date: new Date().toISOString(),
            id: uuid(),
            title: `Moving ${items.length} item(s)...`,
        };
        await executeOp(op);
    }, [executeOp]);
    const createItem = useCallback(async (parent, templateId, name) => {
        const op = {
            type: "create-item",
            parentId: parent.id,
            templateId: templateId,
            name: name,
            mainItem: stateRef.current.page?.item.descriptor || getItemDescriptor(parent),
            date: new Date().toISOString(),
            id: uuid(),
            title: `Create ${name}`,
            canUndo: true,
        };
        const result = (await executeOp(op));
        if (!result || !result.createdItemId) {
            return;
        }
        itemsRepository.refreshItems([parent]);
        return {
            id: result.createdItemId,
            language: parent.language,
            version: 1,
        };
    }, [itemsRepository, executeOp]);
    const createVersion = useCallback(async (item) => {
        const op = {
            type: "add-version",
            item: getItemDescriptor(item),
            mainItem: stateRef.current.page?.item.descriptor || getItemDescriptor(item),
            date: new Date().toISOString(),
            id: uuid(),
            title: `Add version to ${item.id}`,
            canUndo: true,
        };
        await executeOp(op);
    }, [executeOp]);
    const removeVersion = useCallback(async (item) => {
        const op = {
            type: "remove-version",
            item: getItemDescriptor(item),
            version: item.version,
            mainItem: stateRef.current.page?.item.descriptor || getItemDescriptor(item),
            date: new Date().toISOString(),
            id: uuid(),
            title: `Remove version ${item.version} from ${item.id}`,
            canUndo: true,
        };
        await executeOp(op);
    }, [executeOp]);
    const copyItems = useCallback(async (items, target, index) => {
        // Use generic edit endpoint for long-running operation support
        const op = {
            type: "copy-items",
            sourceItemIds: items.map((i) => i.id),
            targetParentId: target.id,
            targetIndex: index >= 0 ? index : undefined,
            mainItem: getItemDescriptor(target),
            date: new Date().toISOString(),
            id: uuid(),
            title: `Copying ${items.length} item(s)...`,
        };
        let copyResult = (await executeOp(op));
        if (copyItemsNeedsTerminalCompletion(copyResult)) {
            try {
                const terminal = await waitForEditOperationTerminal(copyResult.id, 120_000);
                copyResult = { ...copyResult, ...terminal };
            }
            catch {
                return [];
            }
        }
        // Return created item descriptors from the operation result
        if (copyResult?.createdItemIds) {
            return copyResult.createdItemIds.map((id) => ({
                id,
                language: target.language,
                version: 0,
            }));
        }
        return [];
    }, [executeOp]);
    const cloneItems = useCallback(async (items, target, index) => {
        const op = {
            type: "clone-items",
            sourceItemIds: items.map((i) => i.id),
            targetParentId: target.id,
            targetIndex: index >= 0 ? index : undefined,
            mainItem: getItemDescriptor(target),
            date: new Date().toISOString(),
            id: uuid(),
            title: `Cloning ${items.length} item(s)...`,
        };
        let cloneResult = (await executeOp(op));
        if (copyItemsNeedsTerminalCompletion(cloneResult)) {
            try {
                const terminal = await waitForEditOperationTerminal(cloneResult.id, 120_000);
                cloneResult = { ...cloneResult, ...terminal };
            }
            catch {
                // No terminal message within the timeout; the central lifecycle
                // toast keeps tracking the operation
                return [];
            }
        }
        if (cloneResult?.errorMessage) {
            // operation:failed already produced the central error toast
            return [];
        }
        if (cloneResult?.createdItemIds) {
            return cloneResult.createdItemIds.map((id) => ({
                id,
                language: target.language,
                version: 0,
            }));
        }
        return [];
    }, [executeOp]);
    const duplicateItem = useCallback(async (item, target, name) => {
        // Use generic edit endpoint for long-running operation support
        // Duplicate is a copy to the same parent, positioned after the original
        const op = {
            type: "copy-items",
            sourceItemIds: [item.id],
            targetParentId: target.id,
            targetIndex: undefined, // Will be positioned after source by backend
            mainItem: getItemDescriptor(target),
            date: new Date().toISOString(),
            id: uuid(),
            title: `Duplicating item...`,
            name, // Pass the user-provided name to the backend
        };
        let copyResult = (await executeOp(op));
        if (copyItemsNeedsTerminalCompletion(copyResult)) {
            try {
                const terminal = await waitForEditOperationTerminal(copyResult.id, 120_000);
                copyResult = { ...copyResult, ...terminal };
            }
            catch {
                // No terminal message within the timeout; the central lifecycle
                // toast keeps tracking the operation
                return;
            }
        }
        if (copyResult.errorMessage) {
            // operation:failed already produced the central error toast
            return;
        }
        itemsRepository.refreshItems([target]);
        // Return created item descriptor from the operation result
        if (copyResult?.createdItemIds?.length > 0) {
            return {
                id: copyResult.createdItemIds[0],
                language: target.language,
                version: 0,
            };
        }
        return undefined;
    }, [executeOp, itemsRepository]);
    const publishItem = useCallback(async (operation) => {
        const result = (await executeOp(operation, {
            refresh: "immediate",
        }));
        return result;
    }, [executeOp]);
    const onFieldBlur = useCallback(() => {
        const editGenerationAtBlur = fieldEditGeneration.current;
        const pendingSave = executeEditFieldDebounced.isPending()
            ? executeEditFieldDebounced.flush()
            : undefined;
        if (pendingSave) {
            void Promise.resolve(pendingSave).finally(() => {
                // Preserve a newer edit that started while the blurred field was saving.
                if (fieldEditGeneration.current === editGenerationAtBlur) {
                    lastOp.current = undefined;
                }
            });
            return;
        }
        // Reset the last operation to force creation of a new operation
        // when the user returns to edit any field after losing focus.
        lastOp.current = undefined;
    }, [executeEditFieldDebounced]);
    // Extract state values to avoid object reference changes
    const mode = stateRef.current.mode;
    const page = stateRef.current.page;
    const ops = useMemo(() => ({
        addComponent,
        ensureLock,
        lockItems: lockItemsAndRefresh,
        unlockItems: unlockItemsAndRefresh,
        executeWorkflowCommand: executeWorkflowCommandAndRefresh,
        editField,
        updateRenderingParameters,
        updateRenderingRules,
        undo,
        undoOperations,
        redo,
        executeEditOperation: executeOp,
        renameItem,
        deleteItems,
        createItem,
        createVersion,
        removeVersion,
        publishItem,
        duplicateComponents,
        undoing: undoing.current,
        moveItems,
        copyItems,
        cloneItems,
        duplicateItem,
        onFieldBlur,
    }), [
        addComponent,
        ensureLock,
        lockItemsAndRefresh,
        unlockItemsAndRefresh,
        executeWorkflowCommandAndRefresh,
        editField,
        updateRenderingParameters,
        updateRenderingRules,
        undo,
        undoOperations,
        redo,
        executeOp,
        renameItem,
        deleteItems,
        createItem,
        createVersion,
        removeVersion,
        publishItem,
        duplicateComponents,
        moveItems,
        mode,
        page,
        copyItems,
        cloneItems,
        duplicateItem,
        onFieldBlur,
    ]);
    // Cleanup function to flush pending debounced operations on unmount
    // This prevents fields from getting stuck in "saving" state when navigating away
    const cleanup = useCallback(() => {
        executeEditFieldDebounced.flush();
        refreshWorkboxAfterWorkflowChange.cancel();
    }, [executeEditFieldDebounced, refreshWorkboxAfterWorkflowChange]);
    // Function to mark an operation as no longer executing
    // Called when WebSocket receives operation:completed or operation:failed
    const markOperationComplete = useCallback((operationId) => {
        const key = normalizeOpId(operationId);
        clearInsertingIndicatorForOperation(operationId);
        clearRemovingIndicatorsForOperation(operationId);
        setExecutingEditOperations((ops) => ops ? ops.filter((x) => normalizeOpId(x.id) !== key) : []);
    }, [clearInsertingIndicatorForOperation, clearRemovingIndicatorsForOperation]);
    // Upsert a long-running operation reported by the WebSocket. Covers ops
    // started in another tab of the same user, which never pass through
    // executeOp in this tab.
    const addExecutingOperation = useCallback((operation) => {
        const key = normalizeOpId(operation.id);
        setExecutingEditOperations((ops) => {
            const list = ops ?? [];
            const existing = list.find((x) => normalizeOpId(x.id) === key);
            if (existing) {
                return list.map((x) => normalizeOpId(x.id) === key ? { ...x, ...operation } : x);
            }
            return [...list, operation];
        });
    }, []);
    // Live progress for an executing operation (from operation:progress messages)
    const updateExecutingOperationProgress = useCallback((operationId, progress, statusMessage) => {
        const key = normalizeOpId(operationId);
        setExecutingEditOperations((ops) => (ops ?? []).map((x) => normalizeOpId(x.id) === key ? { ...x, progress, statusMessage } : x));
    }, []);
    return useMemo(() => ({
        ops,
        context: {
            editOperationExecuted,
            executingEditOperations,
            markOperationComplete,
            addExecutingOperation,
            updateExecutingOperationProgress,
        },
        cleanup,
    }), [
        executingEditOperations,
        editOperationExecuted,
        markOperationComplete,
        addExecutingOperation,
        updateExecutingOperationProgress,
        ops,
        cleanup,
    ]);
}
function removeSuggestedEditFromState(edit, state, keys) {
    // Tombstone first so any in-flight debounced save neither re-creates the
    // suggestion on the server nor re-adds it to local state via its save-ack.
    // The tombstone is permanent for the session: a save scheduled while this
    // removal is settling (e.g. an edit queued behind the in-flight save) would
    // otherwise fire after an early release and resurrect the suggestion.
    markSuggestedEditRemoved(edit.id);
    pendingSuggestedEdits.delete(keys.pendingFieldKey);
    cleanupPendingSuggestedEditOperations(edit, state.user);
    state.setSuggestedEdits((current) => current.filter((candidate) => candidate.id !== edit.id));
    // Delete on the server only after any in-flight save settles, otherwise the
    // delete can race ahead of a pending create and the suggestion comes back.
    void enqueueSuggestedEditFieldTask(keys.saveKey, () => deleteSuggestedEdit(edit))
        .then((result) => {
        if (result.type !== "success") {
            console.warn("Failed to delete no-op suggestion:", result.summary, result.details);
        }
    })
        .catch((error) => {
        console.warn("Failed to delete no-op suggestion:", error);
    });
}
function getSuggestionCommentCount(edit, state) {
    return (state.comments || []).filter((comment) => comment.suggestedEditId === edit.id).length;
}
async function getOrMergeSuggestedEditOp(field, rawValue, value, state) {
    const item = await state.itemsRepository.getItem(field.item);
    if (!item) {
        return;
    }
    const fieldItem = item.fields.find((f) => f.id === field.fieldId);
    if (!fieldItem) {
        console.log("Could not locate field in item for suggested edit:", field.fieldId);
        return;
    }
    const newVal = rawValue !== undefined ? rawValue : value;
    if (newVal === undefined) {
        return;
    }
    const pageDescriptor = state.page?.item.descriptor ?? state.currentItemDescriptor;
    if (!pageDescriptor) {
        return;
    }
    // Keys for the per-field bookkeeping maps. The saves map must use the same
    // key format as editField (the field's own item descriptor — NOT the
    // suggestion's mainItem fields, which hold the page's language/version and
    // diverge for datasource items).
    const saveKey = getSuggestionFieldIdentityKey({
        itemId: field.item.id,
        language: field.item.language,
        version: field.item.version,
        fieldId: field.fieldId,
    });
    const fieldKey = `${saveKey}:${state.user?.name || "unknown"}`;
    const shouldRemoveAsNoOp = (edit, nextValue) => 
    // Comments load asynchronously; deleting on the strength of a not-yet-
    // loaded comments array would destroy another reviewer's thread (the
    // server cascade-deletes a suggestion's comments along with it).
    state.commentsLoaded === true &&
        shouldRemoveNoOpSuggestion(edit.oldValue, nextValue, () => getSuggestionCommentCount(edit, state)) &&
        // Only auto-remove when the typed value is also a no-op against the
        // field's live value: if the field changed underneath the suggestion, the
        // typed content still differs from the page and must stay recorded.
        !hasSuggestionContentChange(fieldItem.rawValue, nextValue);
    // First check if there's a pending suggested edit for this field. A draft
    // whose suggestion was already reverted must not be revived — a new edit
    // mints a fresh suggestion instead.
    let pendingEdit = pendingSuggestedEdits.get(fieldKey);
    if (pendingEdit && isSuggestedEditRemoved(pendingEdit.id)) {
        pendingSuggestedEdits.delete(fieldKey);
        pendingEdit = undefined;
    }
    if (pendingEdit) {
        const nextValue = newVal || "";
        if (shouldRemoveAsNoOp(pendingEdit, nextValue)) {
            removeSuggestedEditFromState(pendingEdit, state, {
                pendingFieldKey: fieldKey,
                saveKey,
            });
            return;
        }
        // Update the pending edit instead of creating a new one.
        const updatedEdit = {
            ...pendingEdit,
            itemLanguage: field.item.language,
            itemVersion: field.item.version,
            newValue: nextValue,
            updated: new Date().toISOString(),
            updatedBy: state.user?.name || "unknown",
        };
        pendingSuggestedEdits.set(fieldKey, updatedEdit);
        state.setSuggestedEdits((current) => upsertSuggestedEdit(current, updatedEdit, { source: "local" }));
        return updatedEdit;
    }
    const matchingSuggestions = state.suggestedEdits.filter((edit) => edit.mainItemId === pageDescriptor.id &&
        edit.mainItemLanguage === pageDescriptor.language &&
        edit.itemId === item.id &&
        getSuggestedEditItemLanguage(edit) === field.item.language &&
        (getSuggestedEditItemVersion(edit) === 0 ||
            getSuggestedEditItemVersion(edit) === field.item.version) &&
        edit.fieldId === field.fieldId);
    // Attempt to find an existing suggested edit by the same user for this field.
    // Tombstoned ids are skipped: the state snapshot can still contain a
    // suggestion that was just reverted, and reusing its id would tie the new
    // edit to a save pipeline that permanently ignores it.
    const existing = matchingSuggestions.find((edit) => edit.mainItemId === pageDescriptor.id &&
        edit.mainItemLanguage === pageDescriptor.language &&
        edit.mainItemVersion === pageDescriptor.version &&
        edit.itemId === item.id &&
        getSuggestedEditItemLanguage(edit) === field.item.language &&
        (getSuggestedEditItemVersion(edit) === 0 ||
            getSuggestedEditItemVersion(edit) === field.item.version) &&
        edit.fieldId === field.fieldId &&
        edit.author === (state.user?.name || "unknown") &&
        edit.status === "pending" && // or any status that indicates it's open for further editing.
        !isSuggestedEditRemoved(edit.id));
    if (existing) {
        const nextValue = newVal || "";
        if (shouldRemoveAsNoOp(existing, nextValue)) {
            removeSuggestedEditFromState(existing, state, {
                pendingFieldKey: fieldKey,
                saveKey,
            });
            return;
        }
        // Update the existing suggestion.
        const updatedEdit = {
            ...existing,
            itemLanguage: field.item.language,
            itemVersion: field.item.version,
            newValue: nextValue,
            updated: new Date().toISOString(),
            updatedBy: state.user?.name || "unknown",
        };
        state.setSuggestedEdits((current) => upsertSuggestedEdit(current, updatedEdit, { source: "local" }));
        return updatedEdit;
    }
    else {
        // Create a new suggested edit if none exists.
        const newValue = newVal || "";
        const oldValue = fieldItem.rawValue || "";
        if (!hasSuggestionContentChange(oldValue, newValue)) {
            return;
        }
        const newEdit = {
            id: uuid(),
            mainItemId: pageDescriptor.id,
            mainItemLanguage: pageDescriptor.language,
            mainItemVersion: pageDescriptor.version,
            itemId: item.id,
            itemLanguage: field.item.language,
            itemVersion: field.item.version,
            fieldId: field.fieldId,
            oldValue: fieldItem.rawValue || "",
            newValue: newVal || "",
            author: state.user?.name || "unknown",
            status: "pending",
            updatedBy: state.user?.name || "unknown",
            created: new Date().toISOString(),
            updated: new Date().toISOString(),
            type: "FieldValue",
        };
        // Add to pending edits to prevent race conditions
        pendingSuggestedEdits.set(fieldKey, newEdit);
        state.setSuggestedEdits((current) => upsertSuggestedEdit(current, newEdit, { source: "local" }));
        return newEdit;
    }
}
//# sourceMappingURL=operations.js.map