import http, { IncomingMessage } from 'http';
import type * as StackbitTypes from '@stackbit/types';

export type FlatDocument = {
    __metadata: Omit<StackbitTypes.Document, 'fields'>;
    [key: string]: any;
};
export type GetCSIDocumentsOptions = {
    stackbitApiKey: string;
    srcType?: string;
    srcProjectId?: string;
    srcDocumentIds?: string[];
    documentSpecs?: StackbitTypes.DocumentSpecifier[];
    limit?: number;
    offset?: number;
};
export async function getCSIDocuments(options: GetCSIDocumentsOptions & { flatDocuments?: never; flatLocale?: never }): Promise<{
    total: number;
    offset: number;
    documents: StackbitTypes.Document[];
}>;
export async function getCSIDocuments(options: GetCSIDocumentsOptions & { flatDocuments?: boolean; flatLocale?: string }): Promise<{
    total: number;
    offset: number;
    documents: FlatDocument[];
}>;
export async function getCSIDocuments({
    stackbitApiKey,
    srcDocumentIds,
    srcType,
    srcProjectId,
    documentSpecs,
    limit,
    offset,
    flatDocuments,
    flatLocale
}: GetCSIDocumentsOptions & { flatDocuments?: boolean; flatLocale?: string }) {
    const response = await new Promise<{
        total: number;
        offset: number;
        documents: StackbitTypes.Document[];
    }>((resolve, reject) => {
        const url = `${process.env.STACKBIT_HOST ?? 'http://localhost:8090'}/_stackbit/getCSIDocuments`;
        const req = http.request(
            url,
            {
                method: 'POST',
                headers: {
                    Authorization: `Bearer ${stackbitApiKey}`,
                    'Content-Type': 'application/json'
                }
            },
            (res: IncomingMessage) => {
                let output: string = '';
                res.setEncoding('utf8');
                res.on('data', (chunk) => {
                    output += chunk;
                });
                res.on('end', () => {
                    resolve(JSON.parse(output));
                });
            }
        );

        req.on('error', (error) => {
            console.log(`error getting documents from stackbit: ${error.message}`);
            reject(error);
        });

        if (srcType && srcProjectId && srcDocumentIds && srcDocumentIds.length > 0) {
            documentSpecs = srcDocumentIds.map((srcDocumentId) => ({
                srcType,
                srcProjectId,
                srcDocumentId
            }));
        }

        if (documentSpecs) {
            // get the specified documents
            req.write(JSON.stringify({ documentSpecs, limit, offset }));
        } else {
            // get all documents, possibly filtered by srcType and/or srcProjectId
            req.write(JSON.stringify({ srcType, srcProjectId, limit, offset }));
        }

        req.end();
    });

    if (flatDocuments) {
        return {
            total: response.total,
            offset: response.offset,
            documents: response.documents.map((document) => flattenDocument({ document, locale: flatLocale }))
        };
    }
    return response;
}

/**
 * Flattens {@link StackbitTypes.Document} into a simplified object.
 * All document properties except `fields` are moved into the `__metadata`
 * property. All document `fields` are placed at the root of the document and
 * recursively simplified by referencing their values directly.
 *
 * If the `locale` argument is not specified, only the non-localized fields will
 * be returned. If the `locale` is specified, only the localized document fields
 * for that locale, and also non-localized fields will be returned.
 *
 * @example
 * flattenDocument({
 *     document: {
 *         type: 'document',
 *         id: 'xyz',
 *         modelName: 'Page',
 *         ...other,
 *         fields: {
 *             title: { type: 'string', value: 'Welcome' },
 *             seo: { type: 'object', fields: { title: { type: 'string', value: 'SEO Welcome' } } },
 *             tags: { type: 'list', items: [{ type: 'string', value: 'tech' }] }
 *         }
 *     }
 * }) => {
 *     __metadata: {
 *         type: 'document',
 *         id: 'xyz',
 *         modelName: 'Page',
 *         ...rest
 *     },
 *     title: 'Welcome',
 *     seo: { title: 'SEO Welcome' },
 *     tags: ['tech']
 * }
 *
 * @param document
 * @param locale
 */
export function flattenDocument({ document, locale }: { document: StackbitTypes.Document; locale?: string }): FlatDocument {
    const { fields, ...rest } = document;
    const flattenedFields = flattenDocumentFields({ documentFields: fields, locale });
    return {
        __metadata: rest,
        ...flattenedFields
    };
}

function flattenDocumentFields({
    documentFields,
    locale
}: {
    documentFields?: Record<string, StackbitTypes.DocumentField>;
    locale?: string;
}): Record<string, any> {
    return Object.entries(documentFields ?? {}).reduce(
        (flattenedFields: Record<string, any>, [fieldName, documentField]: [string, StackbitTypes.DocumentField]) => {
            const value = getDocumentFieldValue({ documentField, locale });
            if (typeof value !== 'undefined') {
                flattenedFields[fieldName] = value;
            }
            return flattenedFields;
        },
        {}
    );
}

/**
 * Returns the value of a document field.
 *
 * If the document field is a primitive (string, url, slug, text, markdown, enum, date, number, boolean, etc.)
 * then the value of the `value` property is returned.
 *
 * If the document is a field of type "object", "model" or "list", the value of the
 * nested object is simplified and returned.
 *
 * If the document is a field of type "reference", the ID of the referenced
 * document is returned.
 *
 * If the document field is localized, then the value for the provided "locale"
 * is returned. If "locale" was not provided, or the document field does not
 * have any values for the provided "locale", undefined is returned.
 */
export function getDocumentFieldValue({ documentField, locale }: { documentField: StackbitTypes.DocumentField; locale?: string }): any {
    const nonLocalizedDocField = getLocalizedFieldForLocale(documentField, locale);
    if (typeof nonLocalizedDocField === 'undefined') {
        return nonLocalizedDocField;
    }
    switch (nonLocalizedDocField.type) {
        case 'string':
        case 'url':
        case 'slug':
        case 'text':
        case 'markdown':
        case 'html':
        case 'enum':
        case 'date':
        case 'datetime':
        case 'color':
        case 'file':
        case 'number':
        case 'boolean':
        case 'json':
        case 'style':
        case 'richText': {
            return nonLocalizedDocField.value;
        }
        case 'image': {
            const imageFields = flattenDocumentFields({
                documentFields: nonLocalizedDocField.fields,
                locale
            });
            return imageFields?.url ?? nonLocalizedDocField.sourceData;
        }
        case 'object': {
            return flattenDocumentFields({
                documentFields: nonLocalizedDocField.fields,
                locale
            });
        }
        case 'model': {
            return {
                __metadata: {
                    modelName: nonLocalizedDocField.modelName
                },
                ...flattenDocumentFields({
                    documentFields: nonLocalizedDocField.fields,
                    locale
                })
            };
        }
        case 'reference': {
            return nonLocalizedDocField.refId;
        }
        case 'cross-reference': {
            return {
                refId: nonLocalizedDocField.refId,
                refSrcType: nonLocalizedDocField.refSrcType,
                refProjectId: nonLocalizedDocField.refProjectId
            };
        }
        case 'list': {
            return nonLocalizedDocField.items.map((item) => {
                return getDocumentFieldValue({ documentField: item, locale });
            });
        }
        default: {
            const _exhaustiveCheck: never = nonLocalizedDocField;
            return _exhaustiveCheck;
        }
    }
}

// This method was copied from @stackbit/types => getLocalizedFieldForLocale
// instead of importing it directly, to allow bundling this file (document-utils.ts)
// by Webpack without bundling the whole @stackbit/types when it is imported
// inside frameworks like Next.js.
function getLocalizedFieldForLocale<Type extends StackbitTypes.FieldType>(
    field?: StackbitTypes.DocumentFieldForType<Type>,
    locale?: string
): StackbitTypes.DocumentFieldNonLocalizedForType<Type> | undefined {
    if (field && field.localized) {
        if (!locale) {
            return undefined;
        }
        const { localized, locales, ...base } = field;
        const localizedField = locales?.[locale];
        if (!localizedField) {
            return undefined;
        }
        return {
            ...base,
            ...localizedField
        } as unknown as StackbitTypes.DocumentFieldNonLocalizedForType<Type>;
    }
    return field;
}
