import _ from 'lodash';
import { Field } from '@stackbit/types';
import * as StackbitTypes from '@stackbit/types';
import { ModelWithContext } from './sanity-schema-converter';

export function resolveLabelFieldForModel(model: any, modelLabelFieldPath: string, fields: Field[]): string {
    let labelField = _.get(model, modelLabelFieldPath, null);
    if (labelField) {
        return labelField;
    }
    // see if there is a field named 'title'
    let titleField = _.find(fields, (field) => field.name === 'title' && ['string', 'text'].includes(field.type));
    if (!titleField) {
        // see if there is a field named 'label'
        titleField = _.find(fields, (field) => field.name === 'label' && ['string', 'text'].includes(field.type));
    }
    if (!titleField) {
        // get the first 'string' field
        titleField = _.find(fields, { type: 'string' });
    }
    if (titleField) {
        labelField = _.get(titleField, 'name');
    }
    return labelField || null;
}

export function getSanityAliasFieldType({ resolvedType, model, modelFieldPath }: { resolvedType: string; model: ModelWithContext; modelFieldPath: string[] }) {
    const fieldAlias = model.context?.fieldAliasMap?.[modelFieldPath.join('.')] ?? [];
    return fieldAlias?.find((alias) => alias.resolvedTypeName === resolvedType)?.origTypeName ?? resolvedType;
}

export function resolvedFieldType({ sanityFieldType, model, modelFieldPath }: { sanityFieldType: string; model: ModelWithContext; modelFieldPath: string[] }) {
    const fieldAlias = model.context?.fieldAliasMap?.[modelFieldPath.join('.')] ?? [];
    return fieldAlias?.find((alias) => alias.origTypeName === sanityFieldType)?.resolvedTypeName ?? sanityFieldType;
}

export function isLocalizedModelField(modelField: StackbitTypes.Field | StackbitTypes.FieldListItems) {
    return 'localized' in modelField && modelField.localized;
}

export interface FieldListMultiItem {
    type: 'list';
    items: StackbitTypes.FieldListItems | StackbitTypes.FieldListItems[];
}

/**
 * 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. The 'name' property is not
 * supported in Stackbit list items, so '@ts-ignore' is used.
 *
 * 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: {...}
 *     }]
 * }
 */
export function getItemTypeForListItem(listItem: any, fieldModel: FieldListMultiItem): StackbitTypes.FieldListItems | null {
    const itemModels = fieldModel.items ?? { type: 'string' };
    // in Sanity, list items may have multiple types, in this case, 'items' will be an array
    if (!_.isArray(itemModels)) {
        return itemModels;
    }
    // Handle primitive list item types
    // For primitive list items, the list will hold the primitive values as is,
    // therefore, use JavaScript's `typeof` to infer the type of the values
    const type = _.get(listItem, '_type');
    if (!type) {
        const type = typeof listItem;
        if (typeIsPrimitive(type)) {
            return { type: type };
        }
        return null;
    }
    if (type === 'reference') {
        return _.find(itemModels, { type: 'reference' }) ?? null;
    } else if (type === 'block') {
        return _.find(itemModels, { type: 'richText' }) ?? null;
    } else {
        return (
            _.find(itemModels, (itemModel) => {
                if (itemModel.type === 'model') {
                    return _.includes(itemModel.models, type);
                } else {
                    // if field was one of base types (object, image, slug, etc.)
                    // and it had a "name" property, then the "_type" will be equal to that name,
                    // otherwise the "_type" will be equal to the base type
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    return itemModel.name === type || itemModel.type === type;
                }
            }) ?? null
        );
    }
}

export function typeIsPrimitive(type: string): type is 'string' | 'number' | 'boolean' {
    return ['string', 'number', 'boolean'].includes(type);
}
