import _ from 'lodash';
import * as StackbitTypes from '@stackbit/types';
import type * as SanityTypes from '@sanity/types';
import { deepMap, omitByNil, omitByUndefined } from '@stackbit/utils';
import { resolveLabelFieldForModel } from './utils';

// Sanity's cloudinary and other 3rd party DRM plugins add the following models.
// These models are removed when converting the schema and the fields referencing
// these models are replaced with our regular "image" field with a "source" property
// matching to the name of the supported DRM.
const thirdPartyImageModels = ['cloudinary.asset', 'cloudinary.assetDerived', 'bynder.asset', 'aprimo.asset', 'aprimo.cdnasset'];
const skipModels = [...thirdPartyImageModels, 'media.tag', 'slug', 'markdown', 'json', 'color', 'hslaColor', 'hsvaColor', 'rgbaColor'];
const internationalizedArrayPrefix = 'internationalizedArray';

export type SchemaContext = null;

export type ModelWithContext = StackbitTypes.Model<ModelContext>;
export type ModelContext = {
    localizedFieldsModelMap?: LocalizedFieldsModelMap;
    fieldAliasMap?: FieldAliasMap;
} | null;

/**
 * Maps model fields to the Internationalized Array types
 * The key is the field path of the field within the model.
 */
export type LocalizedFieldsModelMap = Record<
    string,
    {
        arrayModelName: string;
        arrayValueModelName: string;
    }
>;

export type FieldAliasMap = Record<
    string,
    {
        origTypeName: string;
        resolvedTypeName: string;
    }[]
>;

/**
 * Converts Sanity schema to Netlify Create Schema.
 *
 * @param schema Schema as received from sanity-schema-converter.js
 * @param defaultLocale The default locale specified in the SanityContentSource constructor.
 */
export function convertSchema({
    schema,
    logger,
    defaultLocale
}: {
    schema: { models: SanityTypes.SchemaTypeDefinition[] };
    logger: StackbitTypes.Logger;
    defaultLocale?: string;
}): {
    models: ModelWithContext[];
    locales: StackbitTypes.Locale[];
} {
    // First, skip all Sanity internal models with names starting with "sanity."
    // and models defined in skipModels.
    const filteredModels = schema.models.filter((model) => {
        return !model.name.startsWith('sanity.') && !skipModels.includes(model.name);
    });

    // Split models into three groups:
    // 1. Regular "document" and "objets" models that map to Netlify Create's
    //    "data" and "object" models respectively.
    // 2. Models with names starting with "internationalizedArray...". These are
    //    special models produced by Sanity's internationalized-array plugin
    //    https://www.sanity.io/plugins/internationalized-array
    // 3. Type alias models, which are aliases to regular fields like "array",
    //    "string", etc.
    const {
        i18nModels = [],
        documentAndObjectModels = [],
        typeAliasModels = []
    } = _.groupBy(filteredModels, (model) => {
        if (model.name.startsWith(internationalizedArrayPrefix)) {
            return 'i18nModels';
        } else if (['document', 'object'].includes(model.type)) {
            return 'documentAndObjectModels';
        } else {
            return 'typeAliasModels';
        }
    }) as unknown as {
        i18nModels: SanityTypes.TypeAliasDefinition<string, SanityTypes.IntrinsicTypeName | undefined>[];
        documentAndObjectModels: (SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition)[];
        typeAliasModels: SanityTypes.TypeAliasDefinition<string, SanityTypes.IntrinsicTypeName | undefined>[];
    };

    // Get all locale codes from Internationalized Array models
    const locales = getLocalesFromInternationalizedArrays(i18nModels, defaultLocale);
    // Create map of Internationalized Array
    const { i18nArrayModelMap, i18nValueModelMap } = getLocalizedModelMap(i18nModels);

    const typeAliasMap = _.keyBy(typeAliasModels, 'name');

    const models = documentAndObjectModels.map((model): StackbitTypes.DataModel<ModelContext> | StackbitTypes.ObjectModel<ModelContext> => {
        const localizedFieldsModelMap = {};
        const fieldAliasMap = {};
        try {
            const stackbitModel = mapObjectModel({
                model,
                modelFieldPath: [],
                typeAliasMap,
                i18nArrayModelMap,
                localizedFieldsModelMap,
                fieldAliasMap,
                seenAliases: []
            });
            let context: ModelContext = null;
            if (!_.isEmpty(localizedFieldsModelMap) || !_.isEmpty(fieldAliasMap)) {
                context = {
                    ...(_.isEmpty(localizedFieldsModelMap) ? null : { localizedFieldsModelMap }),
                    ...(_.isEmpty(fieldAliasMap) ? null : { fieldAliasMap })
                };
            }
            return {
                ...stackbitModel,
                context
            };
        } catch (error: any) {
            logger.error(`Error converting model '${model.name}'. ${error.message}`);
            throw error;
        }
    });
    return {
        models,
        locales
    };
}

type CommonProps = {
    modelFieldPath: string[];
    typeAliasMap: Record<string, any>;
    i18nArrayModelMap: Record<string, any>;
    localizedFieldsModelMap: LocalizedFieldsModelMap;
    fieldAliasMap: FieldAliasMap;
    seenAliases: string[];
};
type SanityObjectFieldOrArrayItem = SanityIntrinsicFieldOrArrayItem<'object'>;
type MapObjectModelOptions<Type extends SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition | SanityObjectFieldOrArrayItem> = {
    model: Type;
} & CommonProps;
function mapObjectModel(
    options: MapObjectModelOptions<SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition>
): StackbitTypes.DataModel | StackbitTypes.ObjectModel;
function mapObjectModel(options: MapObjectModelOptions<SanityObjectFieldOrArrayItem>): StackbitTypes.FieldObject;
function mapObjectModel({
    model,
    ...rest
}: MapObjectModelOptions<SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition | SanityObjectFieldOrArrayItem>):
    | StackbitTypes.DataModel
    | StackbitTypes.ObjectModel
    | StackbitTypes.FieldObject {
    const modelName = _.get(model, 'name', null);
    const modelLabel = _.get(model, 'title', modelName ? _.startCase(modelName) : null);
    const modelDescription = _.get(model, 'description', null);
    const sanityFieldGroups: StackbitTypes.FieldGroupItem[] = _.get(model, 'groups', []).map((group: any) => ({ name: group.name, label: group.title }));
    const fields = _.get(model, 'fields', []);
    const mappedFields = mapObjectFields({
        fields,
        ...rest
    });

    return omitByNil({
        // TODO: ensure type aliases work for documents
        type: getNormalizedModelType(model),
        name: modelName,
        label: modelLabel,
        description: modelDescription,
        labelField: resolveLabelFieldForModel(model, 'preview.select.title', mappedFields),
        fieldGroups: sanityFieldGroups,
        fields: mappedFields
    }) as StackbitTypes.DataModel | StackbitTypes.ObjectModel | StackbitTypes.FieldObject;
}

function mapObjectFields({ fields, modelFieldPath, ...rest }: { fields: SanityTypes.FieldDefinition[] } & CommonProps): StackbitTypes.Field[] {
    return _.map(fields, (field) => {
        return mapField({
            field,
            modelFieldPath: modelFieldPath.concat(field.name),
            ...rest
        });
    });
}

/**
 * Maps Sanity FieldDefinition or ArrayOfType to the {@link StackbitTypes.Field}
 * or the {@link StackbitTypes.FieldListItems} respectively.
 *
 * The `mapField()` can be called for object fields or array items.
 * When called for array items, the 'name' property is optional and the 'hidden'
 * attribute is not present.
 */
function mapField({
    field,
    modelFieldPath,
    typeAliasMap,
    i18nArrayModelMap,
    localizedFieldsModelMap,
    fieldAliasMap,
    seenAliases
}: { field: SanityTypes.FieldDefinition | SanityTypes.ArrayOfType } & CommonProps): StackbitTypes.Field {
    let type = _.get(field, 'type');
    const name = _.get(field, 'name');
    const label = _.get(field, 'title', name ? _.startCase(name) : undefined);
    const modelFieldPathStr = modelFieldPath.join('.');

    let localized: true | undefined;
    // TODO: can the Internationalized Array type have an alias?
    //  e.g.: localizedString an alias to internationalizedArrayString?
    //  { name: 'localizedString', type: 'internationalizedArrayString' }
    if (type in i18nArrayModelMap) {
        // The localization model map can reference a field definition, or a type alias.
        // i18nArrayModelMap['internationalizedArrayString'] => { valueModelName: 'internationalizedArrayStringValue', valueField: { type: 'string', name: 'value' } }
        // i18nArrayModelMap['internationalizedArrayBoolean'] => { valueModelName: 'internationalizedArrayBooleanValue', valueField: { type: 'boolean', name: 'value' } }
        // i18nArrayModelMap['internationalizedArrayInlineReference'] => { valueModelName: 'internationalizedArrayInlineReferenceValue', valueField: { type: 'reference', to: [...], name: 'value' } }
        // i18nArrayModelMap['internationalizedArrayCustomTypeAlias'] => { valueModelName: 'internationalizedArrayCustomTypeAliasValue', valueField: { type: 'customTypeAlias', name: 'value' } }
        // etc.
        if (seenAliases.includes(type)) {
            throw new Error(`Circular Array aliases are not supported, the Array alias '${type}' is recursively referenced in field '${modelFieldPathStr}'.`);
        }
        seenAliases = seenAliases.concat(type);
        localized = true;
        field = {
            ...i18nArrayModelMap[type].valueField,
            ...field,
            type: i18nArrayModelMap[type].valueField.type
        };
        localizedFieldsModelMap[modelFieldPathStr] = {
            arrayModelName: type,
            arrayValueModelName: i18nArrayModelMap[type].valueModelName
        };
        type = field.type;
    }

    const visitedTypes: string[] = [];
    let addedAlias = false;
    while (type in typeAliasMap) {
        if (seenAliases.includes(type)) {
            throw new Error(`Circular Array aliases not supported, the Array alias ${type} is recursively referenced in field ${modelFieldPathStr}.`);
        }
        seenAliases = seenAliases.concat(type);
        // In Sanity, the properties of the field, override the properties of
        // the alias. However, the final field type is the type of the alias.
        if (visitedTypes.includes(type)) {
            throw new Error(`Circular type alias detected in field ${modelFieldPathStr}: ${visitedTypes.join(' => ')} => ${type}.`);
        }
        visitedTypes.push(type);
        field = {
            ...typeAliasMap[type],
            ...field,
            type: typeAliasMap[type].type
        };
        if (!fieldAliasMap[modelFieldPathStr]) {
            fieldAliasMap[modelFieldPathStr] = [];
        }
        const fieldAliases = fieldAliasMap[modelFieldPathStr]!;
        // Non list fields should have only one alias entry.
        // List fields, can have multiple aliases per list item type.
        if (!addedAlias) {
            addedAlias = true;
            fieldAliases.push({
                origTypeName: type,
                resolvedTypeName: field.type
            });
        } else {
            fieldAliases[fieldAliases.length - 1]!.resolvedTypeName = field.type;
        }
        type = field.type;
    }

    const description = _.get(field, 'description');
    const readOnly = _.get(field, 'readOnly');
    const isRequired = _.get(field, 'validation.isRequired');
    const options = _.get(field, 'options');
    const sanityFieldGroup = _.get(field, 'group');
    const group = Array.isArray(sanityFieldGroup) ? sanityFieldGroup[0] : sanityFieldGroup;
    let hidden = _.get(field, 'hidden');

    // compute default and const values
    let defaultValue = convertDefaultValue(_.get(field, 'initialValue'));
    let constValue;
    if (isRequired) {
        if (!_.isUndefined(defaultValue) && hidden) {
            constValue = defaultValue;
            defaultValue = undefined;
        } else if (type === 'string') {
            const optionsList = _.get(options, 'list');
            if (_.isArray(optionsList) && optionsList.length === 1) {
                hidden = true;
                // constValue = _.head(optionsList);
                // constValue = _.get(constValue, 'value', constValue);
                // defaultValue = undefined;
            }
        }
    }

    const extra = convertField({
        field,
        modelFieldPath,
        typeAliasMap,
        i18nArrayModelMap,
        localizedFieldsModelMap,
        fieldAliasMap,
        seenAliases
    });

    return _.assign(
        omitByUndefined({
            type: null,
            name: name,
            label: label,
            description: description,
            group: group,
            required: isRequired || undefined,
            default: defaultValue,
            const: constValue,
            readOnly: readOnly,
            hidden: hidden,
            localized: localized
        }),
        extra
    );
}

function convertDefaultValue(defaultValue: any) {
    if (!_.isPlainObject(defaultValue) && !_.isArray(defaultValue)) {
        return defaultValue;
    }
    return deepMap(
        defaultValue,
        (value) => {
            if (!_.isPlainObject(value)) {
                return value;
            }
            let result = _.omit(value, ['_ref', '_type', '_key']);
            if ('_ref' in value) {
                result['$$ref'] = value._ref;
            } else if ('_type' in value && !['cloudinary.asset', 'bynder.asset', 'aprimo.cdnasset', 'block', 'geopoint'].includes(value._type)) {
                // TODO: instead of using [...].includes(value._type) pass modelMap, and check "value._type in modelMap"
                // only then we know for sure that _type points to the actual model, and not a field type
                if (value._type === 'image') {
                    result = { $$ref: value.asset?._ref };
                } else if (value._type === 'color') {
                    return value?.hex;
                } else {
                    result['$$type'] = value._type;
                }
            }
            return result;
        },
        { iteratePrimitives: false, includeKeyPath: false }
    );
}

function convertField({ field, ...rest }: { field: SanityTypes.FieldDefinition | SanityTypes.ArrayOfType } & CommonProps): StackbitTypes.FieldSpecificProps {
    const type = _.get(field, 'type');
    if (!_.has(fieldConverterMap, type)) {
        return fieldConverterMap.model({ field: field as SanityAliasFieldOrArrayItemDefinition<'model'>, ...rest });
    }
    return _.get(fieldConverterMap, type)({ field, ...rest });
}

function getEnumOptions(field: any) {
    // A list of predefined values that the user can choose from.
    // The array can either include string values ['sci-fi', 'western']
    // or objects [{title: 'Sci-Fi', value: 'sci-fi'}, ...]
    const list = _.get(field, 'options.list', []);
    if (!_.isEmpty(list)) {
        return _.map(list, (item) => {
            return _.has(item, 'title') ? { label: item.title, value: item.value } : item;
        });
    } else {
        return null;
    }
}

type SanityFieldTypes = Exclude<SanityTypes.IntrinsicTypeName, 'document'>;
type AliasFieldTypes = 'model' | 'color' | 'markdown' | 'json' | 'cloudinary.asset' | 'bynder.asset' | 'aprimo.cdnasset';
type SanityIntrinsicFieldOrArrayItem<Type extends SanityFieldTypes> =
    | (SanityTypes.InlineFieldDefinition[Type] & SanityTypes.FieldDefinitionBase)
    | SanityTypes.IntrinsicArrayOfDefinition[Type];
type SanityAliasFieldOrArrayItemDefinition<Type extends string> =
    | (SanityTypes.TypeAliasDefinition<Type, SanityTypes.IntrinsicTypeName | undefined> & SanityTypes.FieldDefinitionBase)
    | SanityTypes.ArrayOfEntry<SanityTypes.TypeAliasDefinition<Type, SanityTypes.IntrinsicTypeName | undefined>>;
const fieldConverterMap: {
    [Type in SanityFieldTypes]: (options: { field: SanityIntrinsicFieldOrArrayItem<Type> } & CommonProps) => StackbitTypes.FieldSpecificProps;
} & {
    [Type in AliasFieldTypes]: (options: { field: SanityAliasFieldOrArrayItemDefinition<Type> } & CommonProps) => StackbitTypes.FieldSpecificProps;
} = {
    string: ({ field }) => {
        const options = getEnumOptions(field);
        if (options) {
            return { type: 'enum', options: options };
        } else {
            return { type: 'string' };
        }
    },
    slug: () => {
        return { type: 'slug' };
    },
    url: () => {
        return { type: 'url' };
    },
    text: () => {
        return { type: 'text' };
    },
    email: () => {
        return { type: 'string' };
    },
    color: () => {
        return { type: 'color' };
    },
    markdown: () => {
        return { type: 'markdown' };
    },
    block: () => {
        return { type: 'richText' };
    },
    number: ({ field }) => {
        const validation = _.get(field, 'validation');
        const isInteger = _.get(validation, 'isInteger');
        return omitByNil({
            type: 'number',
            subtype: isInteger ? 'int' : 'float',
            min: _.get(validation, 'min'),
            max: _.get(validation, 'max')
        });
    },
    boolean: () => {
        return { type: 'boolean' };
    },
    date: () => {
        return { type: 'date' };
    },
    datetime: () => {
        return { type: 'datetime' };
    },
    file: () => {
        return { type: 'file' };
    },
    image: () => {
        return { type: 'image' };
    },
    'cloudinary.asset': () => {
        return { type: 'image', source: 'cloudinary' };
    },
    'bynder.asset': () => {
        return { type: 'image', source: 'bynder' };
    },
    // aprimo.asset is not supported yet. If need to support, we will use ModelContext to differentiate between different
    // sanity types like in datocms
    'aprimo.cdnasset': () => {
        return { type: 'image', source: 'aprimo' };
    },
    geopoint: () => {
        return { type: 'model', models: ['geopoint'] };
    },
    reference: ({ field }) => {
        const toItems = _.castArray(_.get(field, 'to', []));
        return {
            type: 'reference',
            models: _.map(toItems, (item) => item.type)
        };
    },
    crossDatasetReference: ({ field }) => {
        // TODO: implement cross-reference
        //  Sanity crossDatasetReference fields can reference between datasets
        //  of the same project. But Stackbit cross-reference fields cannot
        //  differentiate environments of the same project as the object in the
        //  models array only contains the srcType and srcProjectId
        return {
            type: 'cross-reference',
            models: []
            // models: field.to.map((toItem) => {
            //     return {
            //         modelName: toItem.type,
            //         srcType: 'sanity',
            //         srcProjectId: '...', // the projectId should be the same as the current one
            //         // Stackbit cross-references do not have a way to specify different environment
            //         srcEnvironment: field.dataset
            //     }
            // })
        };
    },
    json: () => {
        return { type: 'json' };
    },
    object: ({ field, ...rest }) => {
        return mapObjectModel({ model: field, ...rest }) as StackbitTypes.FieldObjectProps;
    },
    model: ({ field }) => {
        const type = _.get(field, 'type');
        if (thirdPartyImageModels.includes(type)) {
            const fn = (fieldConverterMap[type as keyof typeof fieldConverterMap] as any) ?? fieldConverterMap.image;
            return fn();
        }
        return {
            type: 'model',
            models: [type]
        };
    },
    /**
     * Sanity 'Array' field type can hold multiple field types.
     *
     * For example, Sanity Arrays can simultaneously include items of `model`
     * and `reference` types. https://www.sanity.io/docs/array-type#wT47gyCx
     *
     * With that, Sanity Arrays cannot include both primitive and complex types:
     * https://www.sanity.io/docs/array-type#fNBIr84P
     *
     * TODO:
     * This is not yet supported by Stackbit's TypeScript types, so the `any`
     * must be used. Additionally, if a Sanity array has multiple types of items one
     * of which is the 'object' type, then it will also have the 'name' property to
     * allow matching 'object' items to their types.
     *
     * However, Stackbit client app should be able to render this types of lists correctly.
     *
     * @example A list that can include items of type 'model', 'reference' and 'object'.
     * {
     *     type: 'list',
     *     items: [{
     *         type: 'model',
     *         models: [...]
     *     }, {
     *         type: 'reference',
     *         models: [...]
     *     }, {
     *         type: 'object',
     *         name: 'nested_object_name',
     *         fields: {...}
     *     }]
     * }
     */
    array: ({ field, ...rest }) => {
        const options = getEnumOptions(field);
        if (options) {
            return {
                type: 'list',
                controlType: 'checkbox',
                items: {
                    type: 'enum',
                    options: options
                }
            };
        }
        const ofItems = _.get(field, 'of', []);
        if (_.some(ofItems, { type: 'block' })) {
            return { type: 'richText' };
        }
        const items = _.map(ofItems, (item, index) => {
            const { modelFieldPath, ...props } = rest;
            const listModelFieldPath = modelFieldPath.concat('items');
            const modelFieldPathStr = listModelFieldPath.join('.');
            // Sanity array fields may consist of anonymous 'object' with names.
            // When creating such objects, the '_type' should be set to their
            // name to identify them among other 'object' types.
            if (item && 'name' in item && item.name && 'type' in item && item.type === 'object') {
                if (!props.fieldAliasMap[modelFieldPathStr]) {
                    props.fieldAliasMap[modelFieldPathStr] = [];
                }
                props.fieldAliasMap[modelFieldPathStr]!.push({
                    origTypeName: item.name,
                    resolvedTypeName: 'object'
                });
            }
            const listItems = mapField({
                field: item,
                modelFieldPath: listModelFieldPath,
                ...props
            });
            return _.omit(listItems, ['name', 'label', 'description', 'group', 'required', 'default', 'const', 'readOnly', 'hidden', 'localized']);
        });
        let modelNames: string[] = [];
        let referenceModelName: string[] = [];
        const consolidatedItems = [];
        _.forEach(items, (item) => {
            const type = _.get(item, 'type');
            // If the converted items have only two properties
            // - type: 'model' and models: [...],
            // - type: 'reference' and models: [...]
            // Then, consolidate all their models under the same 'model' or
            // 'reference' item. Otherwise, if the field has additional properties
            // like, "name", "label", etc., then add it as a separate items.
            if (type === 'model' && _.has(item, 'models') && _.size(item) === 2) {
                modelNames = modelNames.concat(_.get(item, 'models'));
            } else if (type === 'reference' && _.has(item, 'models') && _.size(item) === 2) {
                referenceModelName = referenceModelName.concat(_.get(item, 'models'));
            } else {
                consolidatedItems.push(item);
            }
        });
        if (!_.isEmpty(modelNames)) {
            consolidatedItems.push({
                type: 'model',
                models: modelNames
            });
        }
        if (!_.isEmpty(referenceModelName)) {
            consolidatedItems.push({
                type: 'reference',
                models: referenceModelName
            });
        }
        if (consolidatedItems.length === 1) {
            return {
                type: 'list',
                items: _.head(consolidatedItems)
            } as StackbitTypes.FieldList;
        }
        return {
            type: 'list',
            items: consolidatedItems
        } as unknown as StackbitTypes.FieldList;
    }
};

function getNormalizedModelType(sanityModel: any) {
    const modelType = _.get(sanityModel, 'type');
    return modelType === 'document' ? 'data' : 'object';
}

function getLocalesFromInternationalizedArrays(internationalizedArrayModels: any, defaultLocale?: string): StackbitTypes.Locale[] {
    const localeMap = internationalizedArrayModels
        .filter((model: any) => model.type === 'array')
        .reduce((localeMap: Record<string, string>, model: any) => {
            if (model?.options?.languages && Array.isArray(model?.options?.languages)) {
                return model?.options?.languages.reduce((localeMap: Record<string, string>, locale: { id: string; title: string }) => {
                    if (!(locale.id in localeMap)) {
                        localeMap[locale.id] = locale.title;
                    }
                    return localeMap;
                }, localeMap);
            }
            return localeMap;
        }, {});
    let defaultLocaleFound = false;
    const locales = Object.entries(localeMap).map(([localeId, localeTitle]) => {
        const locale: StackbitTypes.Locale = {
            code: localeId
        };
        if (defaultLocale && localeId === defaultLocale) {
            defaultLocaleFound = true;
            locale.default = true;
        }
        return locale;
    });
    if (!defaultLocaleFound && locales.length > 0) {
        locales[0]!.default = true;
    }
    return locales;
}

function getLocalizedModelMap(i18nModels: any[]): {
    i18nArrayModelMap: Record<
        string,
        {
            valueModelName: string;
            valueField: any;
        }
    >;
    i18nValueModelMap: Record<string, any>;
} {
    const [i18nArrayModels, i18nObjectModels] = _.partition(i18nModels, { type: 'array' });
    const i18nObjectModelsByName = _.keyBy(i18nObjectModels, 'name');
    return _.reduce(
        i18nArrayModels,
        (accum, i18nArrayModel) => {
            if (Array.isArray(i18nArrayModel.of) && i18nArrayModel.of.length === 1) {
                const localizedArrayOfItem = i18nArrayModel.of[0];
                if (localizedArrayOfItem && localizedArrayOfItem.type in i18nObjectModelsByName) {
                    const valueModelName = localizedArrayOfItem.type;
                    const localizedArrayValueModel = i18nObjectModelsByName[valueModelName];
                    const valueField = localizedArrayValueModel.fields[0];
                    // valueField.name === 'value'
                    accum.i18nArrayModelMap[i18nArrayModel.name] = {
                        valueModelName: valueModelName,
                        valueField: valueField
                    };
                    accum.i18nValueModelMap[valueModelName] = valueField;
                }
            }
            return accum;
        },
        {
            i18nArrayModelMap: {} as Record<
                string,
                {
                    valueModelName: string;
                    valueField: any;
                }
            >,
            i18nValueModelMap: {} as Record<string, any>
        }
    );
}
