import type * as StackbitTypes from '@stackbit/types';

/**
 * Takes object of UpdateOperationField and creates a new object by mapping the
 * UpdateOperationField to DocumentField.
 *
 * @param updateOperationFields
 * @param modelName
 * @param getModelByName
 * @param locale
 */
export function createDocumentFieldsWithUpdateOperationFields({
    updateOperationFields,
    modelName,
    getModelByName,
    locale
}: {
    updateOperationFields: Record<string, StackbitTypes.UpdateOperationField>;
    modelName: string;
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
    locale?: string;
}): Record<string, StackbitTypes.DocumentField> {
    try {
        const model = getModelByName(modelName);
        if (!model) {
            throw new Error(`The document's model '${modelName}' was not found.`);
        }
        return Object.entries(updateOperationFields).reduce(
            (documentFields, [fieldName, updateOperationField]): Record<string, StackbitTypes.DocumentField> => {
                const modelField = (model.fields ?? []).find((field) => field.name === fieldName);
                if (!modelField) {
                    throw new Error(`Field '${fieldName}' in model '${modelName}' was not found.`);
                }
                const documentField = convertOperationFieldToDocumentField({
                    operationField: updateOperationField,
                    modelField,
                    getModelByName,
                    locale
                });
                if (typeof documentField !== 'undefined') {
                    documentFields[fieldName] = documentField;
                }
                return documentFields;
            },
            {} as Record<string, StackbitTypes.DocumentField>
        );
    } catch (error: any) {
        throw new Error(`Error creating document. ${error.message}`);
    }
}

/**
 * Updates `document` with array of `UpdateOperation`.
 *
 * This method doesn't mutate the passed `document`, it returns a new deep copied
 * document with the updated data.
 *
 * @param document
 * @param operations
 * @param getModelByName
 */
export function updateDocumentWithOperations({
    document,
    operations,
    getModelByName
}: {
    document: StackbitTypes.Document;
    operations: StackbitTypes.UpdateOperation[];
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
}): StackbitTypes.Document {
    return operations.reduce((document, updateOperation) => {
        return updateDocumentWithOperation({
            document,
            updateOperation,
            getModelByName
        });
    }, document);
}

/**
 * Updates `document` with `updateOperation`.
 *
 * This method doesn't mutate the passed `document`, it returns a new deep copied
 * document with the updated data.
 *
 * @param document
 * @param updateOperation
 * @param getModelByName
 */
export function updateDocumentWithOperation({
    document,
    updateOperation,
    getModelByName
}: {
    document: StackbitTypes.Document;
    updateOperation: StackbitTypes.UpdateOperation;
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
}): StackbitTypes.Document {
    try {
        if (updateOperation.fieldPath.length === 0) {
            throw new Error(`The operation's fieldPath cannot be empty.`);
        }
        const [fieldName, ...fieldPathTail] = updateOperation.fieldPath;
        if (typeof fieldName !== 'string') {
            throw new Error(`The first item in operation's fieldPath must be a string.`);
        }
        const model = getModelByName(document.modelName);
        if (!model) {
            throw new Error(`The document's model '${document.modelName}' was not found.`);
        }
        const modelField = getModelFieldByName(model, fieldName);
        if (!modelField) {
            throw new Error(`The field '${fieldName}' in operation's fieldPath doesn't match any field of model '${model.name}'.`);
        }
        const { [fieldName]: documentField, ...restFields } = document.fields;
        const updatedDocField = updateDocumentFieldAtFieldPath({
            documentField,
            modelField,
            fieldPath: fieldPathTail,
            updateOperation,
            getModelByName
        });
        return {
            ...document,
            fields: {
                ...restFields,
                ...wrapDocumentFieldOrUndefined(fieldName, updatedDocField)
            }
        };
    } catch (error: any) {
        throw new Error(
            `Error updating document '${document.id}' with '${updateOperation.opType}' operation ` +
                `at field path '${updateOperation.fieldPath.join('.')}'. ${error.message}`
        );
    }
}

/**
 * Updates `documentField` at the specified `fieldPath` with the `updateOperation`.
 * This method doesn't mutate the passed `documentField`, it returns a new deep
 * copied document field with the updated data.
 *
 * The `fieldPath` specifies the path from the current `documentField` to the
 * document field that needs to be updated with the operation. The `fieldPath`
 * is always a slice of the `updateOperation.fieldPath`.
 *
 * The `updateOperation.fieldPath` is a path from the root of the document
 * containing the `documentField` to the document field that needs to be updated.
 */
function updateDocumentFieldAtFieldPath<Type extends StackbitTypes.DocumentField>({
    documentField,
    modelField,
    fieldPath,
    updateOperation,
    getModelByName
}: {
    documentField?: Type;
    modelField: StackbitTypes.Field | StackbitTypes.FieldListItems;
    fieldPath: StackbitTypes.FieldPath;
    updateOperation: StackbitTypes.UpdateOperation;
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
}): Type | undefined {
    // If no more items left in the fieldPath, we found our target documentField, update it.
    if (fieldPath.length === 0) {
        return updateDocumentFieldWithOperation({
            documentField,
            updateOperation,
            getModelByName
        });
    }

    if (!documentField) {
        throw new Error(`The field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' points to an undefined document field.`);
    }

    const locale = updateOperation.locale;
    const [fieldName, ...fieldPathTail] = fieldPath;

    if (documentField.type === 'object') {
        if (modelField.type !== 'object') {
            throw new Error(
                `The 'object' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` +
                    `does not match the model field type '${modelField.type}'.`
            );
        }
        if (typeof fieldName !== 'string') {
            throw new Error(`The '${fieldName}' in the field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' must be a string.`);
        }
        const childModelField = getModelFieldByName(modelField, fieldName);
        if (!childModelField) {
            throw new Error(`The '${fieldName}' in the field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' doesn't match any model field.`);
        }
        if (documentField.localized) {
            const {
                localizedValue,
                restLocalizedValues,
                locale: resolvedLocale
            } = getLocalizedValueForLocale<StackbitTypes.DocumentObjectFieldLocalized>({
                documentField,
                fieldPath: getFieldPathPrefix(updateOperation, fieldPath),
                locale
            });
            const { [fieldName]: childDocField, ...restFields } = localizedValue.fields;
            const updatedValue = updateDocumentFieldAtFieldPath({
                documentField: childDocField,
                modelField: childModelField,
                fieldPath: fieldPathTail,
                updateOperation,
                getModelByName
            });
            return {
                ...documentField,
                locales: {
                    ...restLocalizedValues,
                    [resolvedLocale]: {
                        ...localizedValue,
                        fields: {
                            ...restFields,
                            ...wrapDocumentFieldOrUndefined(fieldName, updatedValue)
                        }
                    }
                }
            };
        }
        const { [fieldName]: childDocField, ...restFields } = documentField.fields;
        const updatedValue = updateDocumentFieldAtFieldPath({
            documentField: childDocField,
            modelField: childModelField,
            fieldPath: fieldPathTail,
            updateOperation,
            getModelByName
        });
        return {
            ...documentField,
            fields: {
                ...restFields,
                ...wrapDocumentFieldOrUndefined(fieldName, updatedValue)
            }
        };
    } else if (documentField.type === 'model') {
        if (modelField.type !== 'model') {
            throw new Error(
                `The 'model' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` +
                    `does not match the model field type '${modelField.type}'.`
            );
        }
        if (typeof fieldName !== 'string') {
            throw new Error(`The '${fieldName}' in the field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' must be a string.`);
        }
        if (documentField.localized) {
            const {
                localizedValue,
                restLocalizedValues,
                locale: resolvedLocale
            } = getLocalizedValueForLocale<StackbitTypes.DocumentModelFieldLocalized>({
                documentField,
                fieldPath: getFieldPathPrefix(updateOperation, fieldPath),
                locale
            });
            const model = getModelByName(localizedValue.modelName);
            if (!model) {
                throw new Error(
                    `The 'model' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' ` +
                        `references a non existing model '${localizedValue.modelName}'.`
                );
            }
            const childModelField = getModelFieldByName(model, fieldName);
            if (!childModelField) {
                throw new Error(`The '${fieldName}' in field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' doesn't match any model field.`);
            }
            const { [fieldName]: childDocField, ...restFields } = localizedValue.fields;
            const updatedValue = updateDocumentFieldAtFieldPath({
                documentField: childDocField,
                modelField: childModelField,
                fieldPath: fieldPathTail,
                updateOperation,
                getModelByName
            });
            return {
                ...documentField,
                locales: {
                    ...restLocalizedValues,
                    [resolvedLocale]: {
                        ...localizedValue,
                        fields: {
                            ...restFields,
                            ...wrapDocumentFieldOrUndefined(fieldName, updatedValue)
                        }
                    }
                }
            };
        }
        const model = getModelByName(documentField.modelName);
        if (!model) {
            throw new Error(
                `The 'model' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' ` +
                    `references a non existing model '${documentField.modelName}'.`
            );
        }
        const childModelField = getModelFieldByName(model, fieldName);
        if (!childModelField) {
            throw new Error(`The field '${fieldName}' in field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' doesn't match any model field.`);
        }
        const { [fieldName]: childDocField, ...restFields } = documentField.fields;
        const updatedValue = updateDocumentFieldAtFieldPath({
            documentField: childDocField,
            modelField: childModelField,
            fieldPath: fieldPathTail,
            updateOperation,
            getModelByName
        });
        return {
            ...documentField,
            fields: {
                ...restFields,
                ...wrapDocumentFieldOrUndefined(fieldName, updatedValue)
            }
        };
    } else if (documentField.type === 'list') {
        if (modelField.type !== 'list') {
            throw new Error(
                `The 'list' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` +
                    `does not match the model field type '${modelField.type}'.`
            );
        }
        const itemIndex = fieldName as number;
        if (documentField.localized) {
            const {
                localizedValue,
                restLocalizedValues,
                locale: resolvedLocale
            } = getLocalizedValueForLocale<StackbitTypes.DocumentListFieldLocalized>({
                documentField,
                fieldPath: getFieldPathPrefix(updateOperation, fieldPath),
                locale
            });
            const updatedItem = updateDocumentFieldAtFieldPath({
                documentField: localizedValue.items[itemIndex],
                modelField: modelField.items,
                fieldPath: fieldPathTail,
                updateOperation,
                getModelByName
            });
            if (typeof updatedItem === 'undefined') {
                throw new Error(
                    `Cannot set list item at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` +
                        `to 'undefined', use 'remove' operation to remove list item.`
                );
            }
            return {
                ...documentField,
                locales: {
                    ...restLocalizedValues,
                    [resolvedLocale]: {
                        ...localizedValue,
                        items: splice(localizedValue.items, itemIndex, 1, updatedItem)
                    }
                }
            };
        }
        const updatedItem = updateDocumentFieldAtFieldPath({
            documentField: documentField.items[itemIndex],
            modelField: modelField.items,
            fieldPath: fieldPathTail,
            updateOperation,
            getModelByName
        });
        if (typeof updatedItem === 'undefined') {
            throw new Error(
                `Cannot set list item at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` +
                    `to 'undefined', use 'remove' operation to remove list item.`
            );
        }
        return {
            ...documentField,
            items: splice(documentField.items, itemIndex, 1, updatedItem)
        };
    } else {
        throw new Error(`The partial field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' points to a non-object or non-list field`);
    }
}

function getModelFieldByName(model: StackbitTypes.Model | StackbitTypes.FieldObjectProps, fieldName: string): StackbitTypes.Field | undefined {
    return model.fields?.find((field) => field.name === fieldName);
}

function wrapDocumentFieldOrUndefined(fieldName: string, documentField?: StackbitTypes.DocumentField): Record<string, StackbitTypes.DocumentField> | undefined {
    if (typeof documentField === 'undefined') {
        return undefined;
    }
    return {
        [fieldName]: documentField
    };
}

function getFieldPathPrefixStr(updateOperation: StackbitTypes.UpdateOperation, fieldPath: StackbitTypes.FieldPath): string {
    return getFieldPathPrefix(updateOperation, fieldPath).join('.');
}

function getFieldPathPrefix(updateOperation: StackbitTypes.UpdateOperation, fieldPathTail: StackbitTypes.FieldPath): StackbitTypes.FieldPath {
    return updateOperation.fieldPath.slice(0, updateOperation.fieldPath.length - fieldPathTail.length);
}

function updateDocumentFieldWithOperation<Type extends StackbitTypes.DocumentField>({
    documentField,
    updateOperation,
    getModelByName
}: {
    documentField?: Type;
    updateOperation: StackbitTypes.UpdateOperation;
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
}): Type | undefined {
    const locale = updateOperation.locale;
    switch (updateOperation.opType) {
        case 'set': {
            return convertOperationFieldToDocumentField({
                operationField: updateOperation.field,
                modelField: updateOperation.modelField as StackbitTypes.Field,
                documentField,
                getModelByName,
                locale
            }) as Type;
        }
        case 'unset': {
            if (documentField?.localized) {
                if (!locale) {
                    throw new Error(
                        `The '${documentField.type}' document field at field path '${updateOperation.fieldPath.join('.')}' ` +
                            `is localized, but no locale was provided in the update operation.`
                    );
                }
                // Remove the locale property to unset the value for the specified locale.
                const { [locale]: currentLocale, ...restLocales } = documentField.locales;
                return {
                    ...documentField,
                    locales: {
                        ...restLocales
                    }
                };
            }
            return undefined;
        }
        case 'insert': {
            // Empty lists can be undefined, in this case, create a dummy
            // document list field with empty items to insert the first item
            const documentFieldRes =
                documentField ??
                (updateOperation.modelField.localized
                    ? {
                          type: 'list',
                          localized: true,
                          locales: locale
                              ? {
                                    [locale]: {
                                        locale,
                                        items: []
                                    }
                                }
                              : {}
                      }
                    : {
                          type: 'list',
                          items: []
                      });
            if (documentFieldRes.type !== 'list') {
                throw new Error(`The 'insert' operation can be performed on 'list' document fields only, got '${documentFieldRes.type}'.`);
            }
            const newItem = convertOperationFieldToDocumentField({
                operationField: updateOperation.item,
                modelField: updateOperation.modelField.items,
                getModelByName,
                locale
            });
            if (documentFieldRes.localized) {
                const {
                    localizedValue,
                    restLocalizedValues,
                    locale: resolvedLocale
                } = getLocalizedValueForLocale<StackbitTypes.DocumentListFieldLocalized>({
                    documentField: documentFieldRes,
                    fieldPath: updateOperation.fieldPath,
                    locale
                });
                const insertionIndex = updateOperation.index ?? localizedValue.items.length;
                return {
                    type: 'list',
                    localized: true,
                    locales: {
                        ...restLocalizedValues,
                        [resolvedLocale]: {
                            ...localizedValue,
                            items: splice(localizedValue.items, insertionIndex, 0, newItem)
                        }
                    }
                } as Type;
            }
            const insertionIndex = updateOperation.index ?? documentFieldRes.items.length;
            return {
                type: 'list',
                items: splice(documentFieldRes.items, insertionIndex, 0, newItem)
            } as Type;
        }
        case 'remove': {
            if (documentField?.type !== 'list') {
                throw new Error(`The 'remove' operation can be performed on 'list' document fields only, got '${documentField?.type}'.`);
            }
            if (documentField.localized) {
                const {
                    localizedValue,
                    restLocalizedValues,
                    locale: resolvedLocale
                } = getLocalizedValueForLocale<StackbitTypes.DocumentListFieldLocalized>({
                    documentField,
                    fieldPath: updateOperation.fieldPath,
                    locale
                });
                return {
                    ...documentField,
                    locales: {
                        ...restLocalizedValues,
                        [resolvedLocale]: {
                            ...localizedValue,
                            items: splice(localizedValue.items, updateOperation.index, 1)
                        }
                    }
                };
            }
            return {
                ...documentField,
                items: splice(documentField.items, updateOperation.index, 1)
            };
        }
        case 'reorder': {
            if (documentField?.type !== 'list') {
                throw new Error(`The 'reorder' operation can be performed on 'list' document fields only, got '${documentField?.type}'.`);
            }
            if (documentField.localized) {
                const {
                    localizedValue,
                    restLocalizedValues,
                    locale: resolvedLocale
                } = getLocalizedValueForLocale<StackbitTypes.DocumentListFieldLocalized>({
                    documentField,
                    fieldPath: updateOperation.fieldPath,
                    locale
                });
                return {
                    ...documentField,
                    locales: {
                        ...restLocalizedValues,
                        [resolvedLocale]: {
                            ...localizedValue,
                            items: updateOperation.order.map((newIndex) => localizedValue.items[newIndex]!)
                        }
                    }
                };
            }
            return {
                ...documentField,
                items: updateOperation.order.map((newIndex) => documentField.items[newIndex]!)
            };
        }
        default: {
            const _exhaustiveCheck: never = updateOperation;
            return _exhaustiveCheck;
        }
    }
}

function splice<Type>(array: Type[], start: number, deleteCount: number, ...items: Type[]): Type[] {
    const copy = array.slice();
    copy.splice(start, deleteCount, ...items);
    return copy;
}

function getLocalizedValueForLocale<Type extends StackbitTypes.DocumentFieldLocalized>({
    documentField,
    fieldPath,
    locale
}: {
    documentField: Type;
    fieldPath: StackbitTypes.FieldPath;
    locale?: string;
}): {
    locale: string;
    localizedValue: Type['locales'][string];
    restLocalizedValues: Type['locales'];
} {
    if (!locale) {
        throw new Error(
            `The '${documentField.type}' document field at field path '${fieldPath.join('.')}' ` +
                `is localized, but no locale was provided in the update operation.`
        );
    }
    const { [locale]: localizedValue, ...restLocalizedValues } = documentField.locales;
    if (!localizedValue) {
        throw new Error(
            `The '${documentField.type}' document field at field path '${fieldPath.join('.')}' ` +
                `is localized, but it has no value for the specified locale '${locale}'.`
        );
    }
    return {
        locale,
        localizedValue: localizedValue as Type['locales'][string],
        restLocalizedValues
    };
}

function convertOperationFieldToDocumentField<
    OpField extends StackbitTypes.UpdateOperationListFieldItem,
    DocField extends StackbitTypes.ExtractByType<StackbitTypes.DocumentListFieldItems, OpField['type']>
>({
    operationField,
    modelField,
    getModelByName,
    locale
}: {
    operationField: OpField;
    modelField: StackbitTypes.FieldListItems;
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
    locale?: string;
}): DocField;
function convertOperationFieldToDocumentField<
    OpField extends StackbitTypes.UpdateOperationField,
    DocField extends StackbitTypes.DocumentFieldForType<OpField['type']>
>({
    operationField,
    modelField,
    documentField,
    getModelByName,
    locale
}: {
    operationField: OpField;
    modelField: StackbitTypes.Field;
    documentField?: DocField;
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
    locale?: string;
}): DocField;
function convertOperationFieldToDocumentField({
    operationField,
    modelField,
    documentField,
    getModelByName,
    locale
}: {
    operationField: StackbitTypes.UpdateOperationField;
    modelField: StackbitTypes.Field | StackbitTypes.FieldListItems;
    documentField?: StackbitTypes.DocumentField;
    getModelByName: (modelName: string) => StackbitTypes.Model | undefined;
    locale?: string;
}): StackbitTypes.DocumentField {
    const localized = 'localized' in modelField && modelField.localized;
    if (localized && !locale) {
        throw new Error(`The '${modelField.type}' field is localized, but it has no value for the specified locale '${locale}'.`);
    }
    if (documentField && documentField.type !== operationField.type) {
        throw new Error(`The updateOperation.field.type '${operationField.type}' doesn't match the target document field type '${documentField.type}'.`);
    }
    const documentFieldLocales = documentField && 'locales' in documentField ? documentField.locales : null;
    switch (operationField.type) {
        case 'string':
        case 'url':
        case 'slug':
        case 'text':
        case 'markdown':
        case 'html':
        case 'number':
        case 'boolean':
        case 'date':
        case 'datetime':
        case 'color':
        case 'json':
        case 'richText':
        case 'file':
        case 'enum':
        case 'style': {
            if (localized) {
                return {
                    type: operationField.type,
                    localized: true,
                    locales: {
                        ...documentFieldLocales,
                        [locale!]: {
                            locale,
                            value: operationField.value
                        }
                    }
                } as StackbitTypes.DocumentFieldLocalized;
            }
            return {
                type: operationField.type,
                value: operationField.value
            };
        }
        case 'image': {
            if (modelField.type !== 'image') {
                throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`);
            }
            if (localized) {
                return {
                    type: operationField.type,
                    localized: true,
                    source: modelField.source,
                    locales: {
                        ...documentFieldLocales,
                        [locale!]: {
                            locale,
                            sourceData: operationField.value
                        }
                    }
                } as StackbitTypes.DocumentImageFieldLocalized;
            }
            return {
                type: operationField.type,
                source: modelField.source,
                sourceData: operationField.value
            };
        }
        case 'object': {
            if (modelField.type !== 'object') {
                throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`);
            }
            const updatedFields = Object.entries(operationField.fields).reduce(
                (fields: Record<string, StackbitTypes.DocumentField>, [fieldName, operationField]) => {
                    const childModelField = modelField.fields.find((field) => field.name === fieldName);
                    if (!childModelField) {
                        throw new Error(`Model field with name '${fieldName}' was not found.`);
                    }
                    fields[fieldName] = convertOperationFieldToDocumentField({
                        operationField,
                        modelField: childModelField,
                        getModelByName,
                        locale
                    });
                    return fields;
                },
                {}
            );
            if (localized) {
                return {
                    type: operationField.type,
                    localized: true,
                    locales: {
                        ...documentFieldLocales,
                        [locale!]: {
                            locale,
                            fields: updatedFields
                        }
                    }
                } as StackbitTypes.DocumentObjectFieldLocalized;
            }
            return {
                type: operationField.type,
                fields: updatedFields
            };
        }
        case 'model': {
            if (modelField.type !== 'model') {
                throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`);
            }
            const model = getModelByName(operationField.modelName) as StackbitTypes.Model;
            if (!model) {
                throw new Error(`Model with name '${operationField.modelName}' not found.`);
            }
            const modelFields = model.fields ?? [];
            const updatedFields = Object.entries(operationField.fields).reduce(
                (fields: Record<string, StackbitTypes.DocumentField>, [fieldName, operationField]) => {
                    const childModelField = modelFields.find((field) => field.name === fieldName);
                    if (!childModelField) {
                        throw new Error(`Model field with name '${fieldName}' was not found in model '${model.name}'.`);
                    }
                    fields[fieldName] = convertOperationFieldToDocumentField({
                        operationField,
                        modelField: childModelField,
                        getModelByName,
                        locale
                    });
                    return fields;
                },
                {}
            );
            if (localized) {
                return {
                    type: operationField.type,
                    localized: true,
                    locales: {
                        ...documentFieldLocales,
                        [locale!]: {
                            locale,
                            modelName: operationField.modelName,
                            fields: updatedFields
                        }
                    }
                } as StackbitTypes.DocumentModelFieldLocalized;
            }
            return {
                type: operationField.type,
                modelName: operationField.modelName,
                fields: updatedFields
            };
        }
        case 'reference': {
            if (localized) {
                return {
                    type: operationField.type,
                    refType: operationField.refType,
                    localized: true,
                    locales: {
                        ...documentFieldLocales,
                        [locale!]: {
                            locale,
                            refId: operationField.refId
                        }
                    }
                } as StackbitTypes.DocumentReferenceFieldLocalized;
            }
            return {
                type: operationField.type,
                refType: operationField.refType,
                refId: operationField.refId
            };
        }
        case 'cross-reference': {
            if (localized) {
                return {
                    type: operationField.type,
                    refType: 'document',
                    localized: true,
                    locales: {
                        ...documentFieldLocales,
                        [locale!]: {
                            locale,
                            refSrcType: operationField.value.refSrcType,
                            refProjectId: operationField.value.refProjectId,
                            refId: operationField.value.refId
                        }
                    }
                } as StackbitTypes.DocumentCrossReferenceFieldLocalized;
            }
            return {
                type: operationField.type,
                refType: 'document',
                refSrcType: operationField.value.refSrcType,
                refProjectId: operationField.value.refProjectId,
                refId: operationField.value.refId
            };
        }
        case 'list': {
            if (modelField.type !== 'list') {
                throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`);
            }
            const items = operationField.items.map((item: StackbitTypes.UpdateOperationListFieldItem) => {
                return convertOperationFieldToDocumentField({
                    operationField: item,
                    modelField: modelField.items,
                    getModelByName,
                    locale
                });
            });
            if (localized) {
                return {
                    type: operationField.type,
                    localized: true,
                    locales: {
                        ...documentFieldLocales,
                        [locale!]: {
                            locale,
                            items
                        }
                    }
                } as StackbitTypes.DocumentListFieldLocalized;
            }
            return {
                type: operationField.type,
                items
            };
        }
        default: {
            const _exhaustiveCheck: never = operationField;
            return _exhaustiveCheck;
        }
    }
}
