import _ from 'lodash';
import { SanityDocument } from '@sanity/client';
import * as StackbitTypes from '@stackbit/types';
import { DocumentHistory, DocumentHistoryMap } from './sanity-api-client';
import { SchemaContext, ModelWithContext, ModelContext } from './sanity-schema-converter';
import { getItemTypeForListItem, isLocalizedModelField, resolvedFieldType } from './utils';

export interface SanitySchedule {
    author: string;
    action: ScheduleAction;
    createdAt: string;
    dataset: string;
    description: string;
    documents: {
        documentId: string;
        documentType?: string;
    }[];
    executeAt: string | null;
    executedAt?: string;
    id: string;
    name: string;
    projectId: string;
    state: ScheduleState;
    stateReason: string;
}

export type ScheduleAction = 'publish' | 'unpublish';

export type ScheduleState = 'cancelled' | 'scheduled' | 'succeeded';

export type DocumentContext = {
    publishedDocument?: SanityDocument;
    draftDocument?: SanityDocument;
};

export type AssetContext = DocumentContext;

export type ContextualDocument = StackbitTypes.Document<DocumentContext>;
export type ContextualAsset = StackbitTypes.Asset<AssetContext>;

export const DRAFT_ID_PREFIX = 'drafts.';

export type GetModelByName = StackbitTypes.Cache<SchemaContext, DocumentContext, AssetContext, ModelContext>['getModelByName'];

export type ConvertDocumentsOptions = {
    documents: SanityDocument[];
    getModelByName: GetModelByName;
    documentsHistory?: DocumentHistoryMap;
    studioUrl?: string;
};

export function getPureObjectId(objectId: string) {
    return objectId.replace(/^drafts\./, '');
}

export function getDraftObjectId(objectId: string) {
    return isDraftId(objectId) ? objectId : `${DRAFT_ID_PREFIX}${objectId}`;
}

export function isDraftId(objectId: string) {
    return objectId && objectId.startsWith(DRAFT_ID_PREFIX);
}

export function convertAndFilterScheduledActions(sanitySchedules: SanitySchedule[]): StackbitTypes.ScheduledAction[] {
    const allScheduledActions = sanitySchedules.map((schedule: SanitySchedule) => convertScheduledAction(schedule));
    return filterScheduledActions(allScheduledActions);
}
export function convertScheduledAction(sanitySchedule: SanitySchedule): StackbitTypes.ScheduledAction {
    return {
        id: sanitySchedule.id,
        name: sanitySchedule.name,
        state: getScheduledActionState(sanitySchedule),
        action: sanitySchedule.action,
        createdAt: sanitySchedule.createdAt,
        createdBy: sanitySchedule.author,
        executeAt: sanitySchedule.executeAt || sanitySchedule.executedAt || '',
        documentIds: sanitySchedule.documents.map((doc: { documentId: string }) => doc.documentId)
    };
}

export function filterScheduledActions(scheduledActions: StackbitTypes.ScheduledAction[]) {
    const cutoffDate = new Date();
    cutoffDate.setMonth(cutoffDate.getMonth() - 1);
    const cutoffDateStr = cutoffDate.toISOString();
    return scheduledActions.filter(
        (scheduledAction: StackbitTypes.ScheduledAction) => scheduledAction.state !== 'cancelled' && scheduledAction.executeAt.localeCompare(cutoffDateStr) > 0
    );
}

export function convertDocuments({ documents, getModelByName, documentsHistory, studioUrl }: ConvertDocumentsOptions): ContextualDocument[] {
    const publishedDocuments = _.keyBy(
        documents.filter((document) => !isDraftId(document._id)),
        '_id'
    );
    const draftDocuments = _.keyBy(
        documents.filter((document) => isDraftId(document._id)),
        (document) => getPureObjectId(document._id)
    );
    return _.uniq([..._.keys(publishedDocuments), ..._.keys(draftDocuments)])
        .map((documentId) => {
            const docId = draftDocuments[documentId]?._id;
            const draftDocumentHistory = docId ? documentsHistory?.[docId] : undefined;
            return convertDocument({
                publishedDocument: publishedDocuments[documentId],
                draftDocument: draftDocuments[documentId],
                getModelByName,
                documentHistory: draftDocumentHistory,
                studioUrl
            });
        })
        .filter((document): document is ContextualDocument => !!document);
}

export type ConvertDocumentOptions = {
    publishedDocument?: SanityDocument;
    draftDocument?: SanityDocument;
    getModelByName: GetModelByName;
    documentHistory?: DocumentHistory[];
    studioUrl?: string;
};

function convertDocument({
    publishedDocument,
    draftDocument,
    getModelByName,
    documentHistory,
    studioUrl
}: ConvertDocumentOptions): ContextualDocument | undefined {
    const document = draftDocument || publishedDocument;
    if (!document) {
        return;
    }
    const model = getModelByName(document._type);
    if (!model) {
        return;
    }
    const pureObjectId = getPureObjectId(document._id);
    const manageUrl = studioUrl ? `${studioUrl}/desk/${model.name};${pureObjectId}` : '';
    return {
        type: 'document',
        id: pureObjectId,
        manageUrl,
        modelName: model.name,
        ...commonFields({ publishedDocument, draftDocument, documentHistory }),
        fields: convertFields({
            object: document,
            modelFields: model.fields,
            rootModel: model,
            modelFieldPath: [],
            getModelByName
        }),
        context: {
            draftDocument,
            publishedDocument
        }
    };
}

export type ConvertAssetsOptions = {
    assets: SanityDocument[];
    documentsHistory: DocumentHistoryMap;
};

export function convertAssets({ assets, documentsHistory }: ConvertAssetsOptions): ContextualAsset[] {
    const publishedDocuments = _.keyBy(
        assets.filter((document) => !isDraftId(document._id)),
        '_id'
    );
    const draftDocuments = _.keyBy(
        assets.filter((document) => isDraftId(document._id)),
        (document) => getPureObjectId(document._id)
    );
    return _.uniq([..._.keys(publishedDocuments), ..._.keys(draftDocuments)]).map((documentId): ContextualAsset => {
        const publishedDocument = publishedDocuments[documentId];
        const draftDocument = draftDocuments[documentId];
        const document = (draftDocument || publishedDocument)!;
        const pureObjectId = getPureObjectId(document._id);
        const draftDocId = draftDocument?._id;
        const draftDocumentHistory = draftDocId ? documentsHistory[draftDocId] : undefined;
        return {
            type: 'asset',
            id: pureObjectId,
            manageUrl: document.url,
            ...commonFields({ draftDocument, publishedDocument, documentHistory: draftDocumentHistory }),
            fields: {
                title: {
                    type: 'string',
                    value: document.originalFilename
                },
                file: {
                    type: 'assetFile',
                    url: document.url,
                    fileName: document.originalFilename,
                    contentType: document.mimeType,
                    dimensions: document.metadata?.dimensions
                }
            },
            context: {
                publishedDocument,
                draftDocument
            }
        };
    });
}

export type ConvertFieldsOptions = {
    object: Record<string, any>;
    modelFields?: StackbitTypes.Field[];
    rootModel: ModelWithContext;
    modelFieldPath: string[];
    getModelByName: GetModelByName;
};

function convertFields({ object, modelFields, rootModel, modelFieldPath, getModelByName }: ConvertFieldsOptions): Record<string, StackbitTypes.DocumentField> {
    const fieldsByName = _.keyBy(modelFields, 'name');
    const result: Record<string, StackbitTypes.DocumentField> = {};
    for (const fieldName in fieldsByName) {
        const value = object[fieldName];
        const modelField = fieldsByName[fieldName];
        if (!modelField) {
            continue;
        }
        const field = convertFieldType({
            value,
            modelField,
            rootModel,
            modelFieldPath: modelFieldPath.concat(fieldName),
            getModelByName
        });
        if (field) {
            result[fieldName] = field;
        }
    }
    return result;
}

/**
 * Converts Sanity's document or object field value, or list item value, to
 * Stackbit's DocumentField or DocumentListFieldItems.
 *
 * The convertFieldType has two overloads:
 * When modelField is Field, it returns DocumentField or undefined.
 * When modelField is FieldListItems it returns DocumentListFieldItems or undefined.
 */
function convertFieldType(options: {
    value: any;
    modelField: StackbitTypes.Field;
    rootModel: ModelWithContext;
    modelFieldPath: string[];
    getModelByName: GetModelByName;
}): StackbitTypes.DocumentField | undefined;
function convertFieldType(options: {
    value: any;
    modelField: StackbitTypes.FieldListItems;
    rootModel: ModelWithContext;
    modelFieldPath: string[];
    getModelByName: GetModelByName;
}): StackbitTypes.DocumentListFieldItems | undefined;
function convertFieldType({
    value,
    modelField,
    rootModel,
    modelFieldPath,
    getModelByName
}: {
    value: any;
    modelField: StackbitTypes.Field | StackbitTypes.FieldListItems;
    rootModel: ModelWithContext;
    modelFieldPath: string[];
    getModelByName: GetModelByName;
}): StackbitTypes.DocumentField | undefined {
    switch (modelField.type) {
        case 'string':
        case 'text':
        case 'html':
        case 'url':
        case 'boolean':
        case 'number':
        case 'date':
        case 'datetime':
        case 'enum':
        case 'json':
        case 'style':
        case 'markdown': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: { type: modelField.type },
                documentFieldSpecificProps: (value: any) => {
                    if (_.isUndefined(value)) {
                        return undefined;
                    }
                    return { value };
                }
            });
        }
        case 'cross-reference': {
            return undefined;
        }
        case 'list': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: { type: 'list' },
                documentFieldSpecificProps: (value: any) => {
                    return {
                        items: _.reduce(
                            value,
                            (accum: StackbitTypes.DocumentListFieldItems[], item) => {
                                const itemModel = getItemTypeForListItem(item, modelField);
                                if (!itemModel) {
                                    return accum;
                                }
                                const documentField = convertFieldType({
                                    value: item,
                                    modelField: itemModel,
                                    rootModel,
                                    modelFieldPath: modelFieldPath.concat('items'),
                                    getModelByName
                                });
                                if (!documentField) {
                                    return accum;
                                }
                                return accum.concat(documentField);
                            },
                            []
                        )
                    };
                }
            });
        }
        case 'object': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: { type: 'object' },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    return {
                        fields: convertFields({
                            object: value,
                            modelFields: modelField.fields,
                            rootModel,
                            modelFieldPath,
                            getModelByName
                        })
                    };
                }
            });
        }
        case 'model': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: { type: 'model' },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    const modelName = resolvedFieldType({ sanityFieldType: value._type, model: rootModel, modelFieldPath });
                    const model = getModelByName(modelName);
                    if (!model) {
                        return undefined;
                    }
                    return {
                        modelName: model.name,
                        fields: convertFields({
                            object: value,
                            modelFields: model.fields,
                            rootModel: model,
                            modelFieldPath: [],
                            getModelByName
                        })
                    };
                }
            });
        }
        case 'reference': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: {
                    type: 'reference',
                    refType: 'document'
                },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    let refId = _.get(value, '_ref', null);
                    if (refId) {
                        refId = refId.replace(/^drafts\./, '');
                    }
                    return {
                        refId
                    };
                }
            });
        }
        case 'color': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: { type: 'color' },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    return {
                        value: _.get(value, 'hex')
                    };
                }
            });
        }
        case 'richText': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: { type: 'richText' },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    return {
                        value: value,
                        hint: flattenRichText(value).substring(0, 200)
                    };
                }
            });
        }
        case 'image': {
            if (modelField.source && ['cloudinary', 'aprimo', 'bynder'].includes(modelField.source)) {
                return createDocumentField({
                    fieldValue: value,
                    modelField: modelField,
                    documentFieldBaseProps: {
                        type: 'image',
                        source: modelField.source
                    },
                    documentFieldSpecificProps: (value: any) => {
                        if (!value) {
                            return undefined;
                        }
                        return {
                            sourceData: value,
                            ...(modelField.source === 'bynder'
                                ? {
                                      fields: {
                                          title: {
                                              type: 'string',
                                              value: value?.name
                                          },
                                          url: {
                                              type: 'string',
                                              value: value?.previewImg
                                          }
                                      }
                                  }
                                : null)
                        };
                    }
                });
            }
            return createDocumentField({
                fieldValue: value,
                // The modelField.type and documentField.type should match.
                // However, in the case of the modelField.type === 'image',
                // it is OK to have the documentField.type === 'reference'
                // and documentField.type === 'asset.
                modelField: modelField as unknown as StackbitTypes.FieldReference,
                documentFieldBaseProps: {
                    type: 'reference',
                    refType: 'asset'
                },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    return {
                        refId: _.get(value, 'asset._ref')
                    };
                }
            });
        }
        case 'file': {
            return createDocumentField({
                fieldValue: value,
                // The modelField.type and documentField.type should match.
                // However, in the case of the modelField.type === 'file',
                // it is OK to have the documentField.type === 'reference'
                // and documentField.type === 'asset.
                modelField: modelField as unknown as StackbitTypes.FieldReference,
                documentFieldBaseProps: {
                    type: 'reference',
                    refType: 'asset'
                },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    return {
                        refId: _.get(value, 'asset._ref')
                    };
                }
            });
        }
        case 'slug': {
            return createDocumentField({
                fieldValue: value,
                modelField: modelField,
                documentFieldBaseProps: { type: 'slug' },
                documentFieldSpecificProps: (value: any) => {
                    if (!value) {
                        return undefined;
                    }
                    return {
                        value: _.get(value, 'current')
                    };
                }
            });
        }
        default: {
            const _exhaustiveCheck: never = modelField;
            return _exhaustiveCheck;
        }
    }
}

type CommonFieldsType = {
    documentHistory?: DocumentHistory[];
    draftDocument?: SanityDocument;
    publishedDocument?: SanityDocument;
};

function commonFields({ draftDocument, publishedDocument, documentHistory }: CommonFieldsType): {
    status: StackbitTypes.Document['status'];
    createdAt: string;
    updatedAt: string;
    updatedBy: string[];
} {
    const document = draftDocument ?? publishedDocument;

    if (!document) {
        throw new Error('No draft or published document found');
    }

    const isDraft = isDraftId(document._id);
    let status: StackbitTypes.Document['status'];
    if (isDraft && !publishedDocument) {
        status = 'added';
    } else {
        status = isDraft ? 'modified' : 'published';
    }
    const updatedByList = documentHistory?.map(({ author }: DocumentHistory) => author);

    return {
        status,
        createdAt: document._createdAt,
        // createdBy: createdByEmail,
        updatedAt: document._updatedAt,
        updatedBy: _.uniq(updatedByList)
    };
}

function createDocumentField<
    Type extends StackbitTypes.FieldType,
    BaseProps extends StackbitTypes.DocumentFieldBasePropsForType<Type>,
    SpecificProps extends StackbitTypes.DocumentFieldSpecificPropsForType<Type>
>({
    fieldValue,
    modelField,
    documentFieldBaseProps,
    documentFieldSpecificProps
}: {
    fieldValue: any;
    modelField: StackbitTypes.FieldForType<Type> | StackbitTypes.FieldListItemsForType<Type>;
    documentFieldBaseProps: BaseProps;
    documentFieldSpecificProps: (value: any) => SpecificProps | undefined;
}): (BaseProps & (SpecificProps | { localized: true; locales: Record<string, { locale: string } & SpecificProps> })) | undefined {
    if (isLocalizedModelField(modelField)) {
        if (!Array.isArray(fieldValue)) {
            return undefined;
        }
        return {
            ...documentFieldBaseProps,
            localized: true,
            locales: _.reduce(
                fieldValue,
                (accum: Record<string, { locale: string } & SpecificProps>, localizedItem: { _key: string; value: any }) => {
                    if (localizedItem === null || typeof localizedItem === 'undefined') {
                        return accum;
                    }
                    const fieldSpecificProps = documentFieldSpecificProps(localizedItem.value);
                    if (!fieldSpecificProps) {
                        return accum;
                    }
                    accum[localizedItem._key] = {
                        locale: localizedItem._key,
                        ...fieldSpecificProps
                    };
                    return accum;
                },
                {}
            )
        };
    }
    const fieldSpecificProps = documentFieldSpecificProps(fieldValue);
    if (!fieldSpecificProps) {
        return undefined;
    }
    return {
        ...documentFieldBaseProps,
        ...fieldSpecificProps
    };
}

function flattenRichText(richTextArray: any) {
    return _.reduce(
        richTextArray,
        (accum, node) => {
            if (_.get(node, '_type') !== 'block') {
                return accum;
            }
            const children: any = _.get(node, 'children', []);
            return (
                accum +
                _.reduce(
                    children,
                    (accum, node) => {
                        if (_.get(node, '_type') !== 'span') {
                            return accum;
                        }
                        return accum + _.get(node, 'text', '');
                    },
                    ''
                )
            );
        },
        ''
    );
}

function getScheduledActionState(sanitySchedule: SanitySchedule): StackbitTypes.ScheduledActionState {
    if (sanitySchedule.state === 'cancelled' && sanitySchedule.stateReason !== 'cancelled by user') {
        return 'failed';
    }
    return sanitySchedule.state;
}
