import type { ContentSourceInterface, ParametersOfCSIMethod, ReturnTypeOfCSIMethod, User } from './content-source';
import type { DocumentWithSource, AssetWithSource } from './content-source-document';
import type { ModelExtension, ModelMatchFields, ModelWithSource, NamelessModelMap } from './models';
import { SSG_NAMES, CMS_NAMES } from './consts';
import type { SidebarButton } from './sidebar-button';
import type { ConfigDelegate } from './config-delegate';
import type { AssetSource } from './asset-source';
import type { CustomActionBulk, CustomActionGlobal, CustomActionModel } from './custom-actions';
import { DocumentPermissions, ModelPermissions } from './content-permissions';
import { ContentObjectSpecification } from './utility-types';

export interface StackbitConfig {
    /**
     * The version of the Stackbit configuration.
     */
    stackbitVersion: string;

    /**
     * The name of the front-end framework.
     *
     * If you specify the `ssgName` property, you can omit the `devCommand`
     * property, and Stackbit will use a default value matching your front-end
     * framework.
     */
    ssgName?: (typeof SSG_NAMES)[number];

    /**
     * The version of your front-end framework.
     * This is particularly useful for Hugo, as it allows for the
     * pre-installation of the matching Hugo binary version.
     */
    ssgVersion?: string;

    /**
     * The Node.js version to use for installing dependencies and running the
     * front-end framework. You should set it to one of the Node major versions
     * such as "18", "16" or "14".
     *
     * @example
     * nodeVersion: "18"
     */
    nodeVersion?: string;

    /**
     * The `useESM` flag indicates if stackbit.config.ts should be compiled to
     * CJS or ESM module.
     *
     * If one of the stackbit.config.ts dependencies is ESM only, you can use
     * this flag to support it.
     *
     * This flag is parsed statically before stackbit.config.ts is
     * loaded as a module.
     */
    useESM?: boolean;

    postGitCloneCommand?: string;
    preInstallCommand?: string;
    postInstallCommand?: string;

    /**
     * The command that should be executed to install the project dependencies.
     *
     * @example
     * installCommand: "npm install"
     */
    installCommand?: string;

    /**
     * The command that should be executed to run the development server of your
     * front-end framework. When specifying the `devCommand` you must specify
     * the arguments for the host and port and set them to `{HOSTNAME}` and
     * `{PORT}` tokens respectively. Stackbit will replace these tokens with its
     * internal host and port.
     *
     * @example
     * devCommand: "node ./node_modules/.bin/gatsby develop -p {PORT} -H {HOSTNAME}
     * devCommand: "node ./node_modules/.bin/next dev --port {PORT} --hostname {HOSTNAME}
     * devCommand: "node custom-server.js --port {PORT} --hostname {HOSTNAME}
     */
    devCommand?: string;

    /** @deprecated This property is deprecated in favor of using `contentSources` */
    cmsName?: (typeof CMS_NAMES)[number];

    import?: Import;
    buildCommand?: string;
    publishDir?: string;

    /**
     * The directory to store the cached content provided by the content-sources.
     * If the cacheDir is a relative path, it will be resolved relative to the
     * stackbit.config.ts file.
     * If not set, defaults to ".stackbit/cache" folder relative to stackbit.config.ts file.
     */
    cacheDir?: string;

    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    staticDir?: string;
    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    uploadDir?: string;
    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    assets?: Assets;
    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    pagesDir?: string | null;
    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    dataDir?: string | null;
    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    pageLayoutKey?: string | null;
    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    objectTypeKey?: string;
    /** @deprecated This property is deprecated in favor of using `GitContentSource` */
    excludePages?: string | string[];

    styleObjectModelName?: string | null;
    presetSource?: PresetSource;

    sidebarButtons?: SidebarButton[];

    /**
     * @alias {@link sitemap}
     * @deprecated use {@link sitemap}
     */
    siteMap?: SiteMapFunction;
    /**
     * The `sitemap` function should return an array of {@link SiteMapEntry}
     * objects. Stackbit uses these objects to show the website's sitemap in its
     * UI.
     *
     * There are two types of sitemap entries: 'static' and 'dynamic'.
     *
     * Static sitemap entries represent static pages on your website and require
     * a [`urlPath`]{@link SiteMapBaseEntry.urlPath}, [`label`]{@link SiteMapBaseEntry.label},
     * and [`stableId`]{@link SiteMapBaseEntry.stableId}. The `stableId` must be
     * a unique identifier that never changes, even if the entry's `urlPath` is
     * modified.
     *
     * Dynamic sitemap entries represent pages on your website that are
     * generated from content source documents. These entries require a
     * [`urlPath`]{@link SiteMapBaseEntry.urlPath} and a
     * [`document`]{@link SiteMapDocumentEntry.document} object with `id`,
     * `modelName`, `srcType`, and `srcProjectId` properties.
     *
     * To generate dynamic sitemap entries, use the `documents` and `models`
     * properties of the `options` parameter.
     *
     * @param {Object} options
     * @param {DocumentWithSource[]} options.documents
     * @param {ModelWithSource[]} options.models
     */
    sitemap?: SiteMapFunction;

    /**
     * The `transformSitemap` method is used to transform sitemap entries available for specific user
     */
    transformSitemap?: (options: ConfigDelegate & { sitemap: SiteMapEntry[]; userContext: User }) => SiteMapEntry[];

    treeViews?: TreeViewsFunction;

    /**
     * The `transformTreeViews` method is used to transform tree view entries available for specific user
     */
    transformTreeViews?: (options: ConfigDelegate & { treeViews: TreeViewNode[]; userContext: User }) => TreeViewNode[];

    presetReferenceBehavior?: 'copyReference' | 'duplicateContents';
    nonDuplicatableModels?: string[];
    duplicatableModels?: string[];

    customContentReload?: boolean;
    experimental?: Experimental;
    contentEngine?: ContentEngineOptions;
    contentSources?: ContentSourceInterface<any, any, any, any, any>[];
    connectors?: {
        integration: any;
        config: any;
    }[];
    assetSources?: AssetSource[];

    viewports?: Viewport[];

    /**
     * The `mapModels` method can be used to modify the models returned by
     * content sources.
     *
     * This method takes an `options` object with the `models` property
     * containing an array of `Model` objects. The `mapModels` method should
     * return an array of modified objects.
     *
     * @param options
     * @param {ModelWithSource[]} options.models
     */
    mapModels?: (options: { models: ModelWithSource[] }) => ModelWithSource[];
    modelExtensions?: ModelExtension[];

    permissionsForModel?: (
        options: ConfigDelegate & { model: ModelWithSource; userContext: User }
    ) => ModelPermissions | undefined;

    // Document hooks
    mapDocuments?: (options: { documents: DocumentWithSource[]; models: ModelWithSource[] }) => DocumentWithSource[];

    permissionsForDocument?: (
        options: ConfigDelegate & {
            document: DocumentWithSource;
            userContext: User;
        }
    ) => DocumentPermissions | undefined;

    /**
     * The `filterAsset` method is used to hide or show asset for specific user
     */
    filterAsset?: (options: ConfigDelegate & { asset: AssetWithSource; userContext: User }) => boolean | undefined;

    /**
     * The `onContentCreate` method can be used to modify the values before
     * they are transformed into operations to create new documents.
     *
     * This method takes a single `options` parameter with an `object` property
     * containing the data needed to create one or more documents with links
     * between themselves.
     *
     * This method should modify the `options.object` and return the modified
     * object.
     *
     * For more information about the structure of the `options.object`, refer
     * to the [`object`]{@link OnContentCreateOptions.object} property documentation
     *
     * @example
     * // Iterate over 'object' fields, for every field of type "slug" sanitize
     * // its value using a custom 'sanitizeSlug' function.
     * mapDocuments: (options) {
     *   const object = options.object;
     *   for (const fieldName of Object.keys(object)) {
     *     const modelField = options.model.fields.find((field) => field.name === fieldName);
     *     if (modelField.type === 'slug') {
     *       const fieldValue = object[fieldName];
     *       object[fieldName] = sanitizeSlug(fieldValue);
     *     }
     *   }
     *   return object;
     * }
     *
     * @param {OnContentCreateOptions} options
     */
    onContentCreate?: (options: OnContentCreateOptions) => Promise<Record<string, any>>;
    onDocumentCreate?: (options: OnDocumentCreateOptions) => ReturnType<ContentSourceInterface['createDocument']>;
    onDocumentUpdate?: (options: OnDocumentUpdateOptions) => ReturnType<ContentSourceInterface['updateDocument']>;
    onDocumentDelete?: (options: OnDocumentDeleteOptions) => ReturnType<ContentSourceInterface['deleteDocument']>;
    onDocumentArchive?: (options: OnDocumentArchiveOptions) => ReturnTypeOfCSIMethod<'archiveDocument'>;
    onDocumentUnarchive?: (options: OnDocumentUnarchiveOptions) => ReturnTypeOfCSIMethod<'unarchiveDocument'>;
    onDocumentsPublish?: (options: OnDocumentsPublishOptions) => ReturnType<ContentSourceInterface['publishDocuments']>;
    onDocumentsUnpublish?: (options: OnDocumentsUnpublishOptions) => ReturnTypeOfCSIMethod<'unpublishDocuments'>;

    /**
     * Use `actions` to extend the visual editor with custom actions that invoke
     * functions specified by the `run` method.
     */
    actions?: (CustomActionGlobal | CustomActionBulk | CustomActionModel)[];

    /**
     * @deprecated This property is deprecated in favor of using `contentSources`
     * and mapping content source models using the `mapModels` and `modelExtensions`
     * properties.
     */
    models?: NamelessModelMap;
    /** @deprecated */
    contentModels?: ContentModelMap;
    /** @deprecated */
    modelsSource?: ModelsSource;
    /** @deprecated */
    logicFields?: string[];
    /** @deprecated */
    noEncodeFields?: string[];
    /** @deprecated */
    omitFields?: string[];
    /** @deprecated */
    encodedFieldTypes?: string[];
}

export type Import = ContentfulImport | SanityImport;

export interface ContentfulImport {
    type: 'contentful';
    contentFile: string;
    uploadAssets?: boolean;
    assetsDirectory?: string;
    spaceIdEnvVar?: string;
    accessTokenEnvVar?: string;
    deliveryTokenEnvVar?: string;
    previewTokenEnvVar?: string;
}

export interface SanityImport {
    type: 'sanity';
    contentFile: string;
    sanityStudioPath: string;
    deployStudio?: boolean;
    deployGraphql?: boolean;
    projectIdEnvVar?: string;
    datasetEnvVar?: string;
    tokenEnvVar?: string;
}

export type Assets = StaticAssets | RelativeAssets;

export interface StaticAssets {
    referenceType: 'static';
    assetsDir?: string;
    staticDir: string;
    publicPath: string;
    uploadDir?: string;
}

export interface RelativeAssets {
    referenceType: 'relative';
    assetsDir: string;
    staticDir?: string;
    publicPath?: string;
    uploadDir?: string;
}

export type ContentModelMap = Record<string, ContentModel>;

export interface ContentModel extends ModelMatchFields {
    isPage?: boolean;
    urlPath?: string;
    hideContent?: boolean;
    newFilePath?: string;
}

export type PresetSource = PresetSourceFiles;

export interface PresetSourceFiles {
    type: 'files';
    presetDirs: string[];
}

export type ModelsSource = ModelsSourceFiles | ModelsSourceContentful | ModelsSourceSanity;

export interface ModelsSourceFiles {
    type: 'files';
    modelDirs: string[];
}

export interface ModelsSourceContentful {
    type: 'contentful';
    module?: string;
    [key: string]: any;
}

export interface ModelsSourceSanity {
    type: 'sanity';
    sanityStudioPath: string;
    module?: string;
    [key: string]: any;
}

export type SiteMapFunction = (options: SiteMapOptions) => SiteMapEntry[];
export type TreeViewsFunction = (options: ConfigDelegate) => Promise<TreeViewNode[]>;

export interface SiteMapOptions extends ConfigDelegate {
    /**
     * When the sitemap function is called for the first time, this property
     * will include all the documents from all the content sources.
     *
     * When a new document is created or an existing document is changed, the
     * sitemap function will be called again and this property will contain only
     * the new and changed documents.
     *
     * To access other documents, use getDocumentById() and getDocuments()
     * functions.
     */
    documents: DocumentWithSource[];
    /**
     * This property includes all the models from all the content sources.
     */
    models: ModelWithSource[];
}

export type SiteMapEntry = SiteMapBaseEntry | SiteMapDocumentEntry;

export interface SiteMapBaseEntry {
    /** The URL path of a page */
    urlPath: string;
    /**
     * The human-readable string describing a page.
     *
     * When used with `document`, the `label` can be omitted, in which case it
     * will be computed using the document model's `labelField` property.
     */
    label: string;
    /**
     * A stable ID that uniquely and consistently represents a website page.
     * If the `urlPath` of a particular page has changed, its stableId must
     * remain the same. The `stableId` allows Stackbit to redirect editors to
     * a different URL when the `urlPath` of the currently viewed page has been
     * changed. For example, one document can represent multiple website pages
     * if it has field-level localization. In this case, the srcDocumentId of
     * both sitemap entries should be the same, however the stableId must be
     * different.
     *
     * When used with `document`, the `stableId` can be omitted, in which case
     * it will be set to document's `id`.
     */
    stableId: string;
    /** The locale of the page */
    locale?: string;
    isHomePage?: boolean;
}

export interface TreeViewBaseNode {
    label: string;
    stableId: string;
    children?: TreeViewNode[];
}

export interface TreeViewDocumentNode {
    label?: string;
    stableId?: string;
    document: {
        srcType: string;
        srcProjectId: string;
        id: string;
    };
    children?: TreeViewNode[];
}

export type TreeViewNode = TreeViewBaseNode | TreeViewDocumentNode;

export interface SiteMapDocumentEntry extends Omit<SiteMapBaseEntry, 'label' | 'stableId'> {
    label?: string;
    stableId?: string;
    document: {
        srcType: string;
        srcProjectId: string;
        modelName: string;
        id: string;
    };
}

export interface ContentEngineOptions {
    port?: number | string;
    host?: string;
}

export interface Experimental {
    ssg?: {
        name?: string;
        passthrough?: string[];
        directRoutes?: Record<string, string>;
        proxyWebsockets?: boolean;
        watch?: {
            reinstallPackages?: string[];
        };
        logPatterns?: {
            up?: string[];
        };
    };
}

export interface Viewport {
    label: string;
    size: {
        width?: number;
        height?: number;
    };
}

export interface OnContentCreateOptions extends ConfigDelegate {
    /**
     * The `object` contains data for creating one or more new documents.
     * It must conform to the {@link ContentObjectSpecification}.
     */
    object: ContentObjectSpecification;
    /**
     * The model representing the new document
     */
    model: ModelWithSource;
    /**
     * The locale for the new document
     */
    locale?: string;
}

export interface OnDocumentCreateOptions extends DocumentHookBaseOptions {
    /**
     * The `options` object is passed to the content source `createDocument`
     * method to create a document. You can transform this object and pass the
     * result to the `createDocument` parameter to create a different document.
     */
    createDocumentOptions: Parameters<ContentSourceInterface['createDocument']>[0];
    /**
     * This function creates a document. Pass the `createDocumentOptions`
     * parameter to this function to create a document. If you call this
     * function without passing any options, original options will be used.
     */
    createDocument: (
        options?: Parameters<ContentSourceInterface['createDocument']>[0]
    ) => ReturnType<ContentSourceInterface['createDocument']>;
}

export interface OnDocumentUpdateOptions extends DocumentHookBaseOptions {
    /**
     * The `options` object is passed to the content source `updateDocument`
     * method to update a document. You can transform this object and pass the
     * result to the `updateDocument` parameter to update the document in a
     * different way.
     */
    updateDocumentOptions: Parameters<ContentSourceInterface['updateDocument']>[0];
    /**
     * This function updates a document. Pass the `updateDocumentOptions`
     * parameter to this function to update a document. If you call this
     * function without passing any options, original options will be used.
     */
    updateDocument: (
        options?: Parameters<ContentSourceInterface['updateDocument']>[0]
    ) => ReturnType<ContentSourceInterface['updateDocument']>;
}

export interface OnDocumentDeleteOptions extends DocumentHookBaseOptions {
    /**
     * The `options` object is passed to the content source `deleteDocument`
     * method to delete a document.
     */
    deleteDocumentOptions: Parameters<ContentSourceInterface['deleteDocument']>[0];
    /**
     * This function deletes a document. Pass the `deleteDocumentOptions`
     * parameter to this function to delete a document. If you call this
     * function without passing any options, original options will be used.
     */
    deleteDocument: (
        options?: Parameters<ContentSourceInterface['deleteDocument']>[0]
    ) => ReturnType<ContentSourceInterface['deleteDocument']>;
}

export interface OnDocumentArchiveOptions extends DocumentHookBaseOptions {
    /**
     * The `options` object is passed to the content source `archiveDocument`
     * method to archive a document.
     */
    archiveDocumentOptions: Parameters<NonNullable<ContentSourceInterface['archiveDocument']>>[0];
    /**
     * This function archives a document. Pass the `archiveDocumentOptions`
     * parameter to this function to archive a document. If you call this
     * function without passing any options, original options will be used.
     */
    archiveDocument: (
        options?: Parameters<NonNullable<ContentSourceInterface['archiveDocument']>>[0]
    ) => ReturnType<NonNullable<ContentSourceInterface['archiveDocument']>>;
}

export interface OnDocumentUnarchiveOptions extends DocumentHookBaseOptions {
    /**
     * The `options` object is passed to the content source `unarchiveDocument`
     * method to unarchive a document.
     */
    unarchiveDocumentOptions: ParametersOfCSIMethod<'unarchiveDocument'>[0];
    /**
     * This function unarchives a document. Pass the `unarchiveDocumentOptions`
     * parameter to this function to unarchive a document. If you call this
     * function without passing any options, original options will be used.
     */
    unarchiveDocument: (
        options?: ParametersOfCSIMethod<'unarchiveDocument'>[0]
    ) => ReturnTypeOfCSIMethod<'unarchiveDocument'>;
}

export interface OnDocumentsPublishOptions extends DocumentHookBaseOptions {
    /**
     * The `options` object is passed to the content source `publishDocuments`
     * method to publish documents.
     */
    publishDocumentsOptions: Parameters<ContentSourceInterface['publishDocuments']>[0];
    /**
     * This function publishes documents. Pass the `publishDocumentsOptions`
     * parameter to this function to publish documents.
     */
    publishDocuments: (
        options?: Parameters<ContentSourceInterface['publishDocuments']>[0]
    ) => ReturnType<ContentSourceInterface['publishDocuments']>;
}
export interface OnDocumentsUnpublishOptions extends DocumentHookBaseOptions {
    /**
     * The `options` object is passed to the content source `unpublishDocuments`
     * method to unpublish documents.
     */
    unpublishDocumentsOptions: ParametersOfCSIMethod<'unpublishDocuments'>[0];
    /**
     * This function publishes documents. Pass the `unpublishDocumentsOptions`
     * parameter to this function to unpublish documents.
     */
    unpublishDocuments: (
        options?: ParametersOfCSIMethod<'unpublishDocuments'>[0]
    ) => ReturnTypeOfCSIMethod<'unpublishDocuments'>;
}

export interface DocumentHookBaseOptions extends ConfigDelegate {
    /**
     * The content source type of the current document
     */
    srcType: string;
    /**
     * The content source project ID of the current document
     */
    srcProjectId: string;
    /**
     * This object contains functions to create, update, delete and publish
     * documents in the content source of the current document.
     *
     * All functions receive an optional `userContext`. If the `userContext`
     * is not specified, a default `userContext` for the current user and the
     * content source will be used.
     *
     * Calling these functions will trigger onDocument hooks. Therefore, do not
     * use these function to create, update, delete and publish the current
     * document, otherwise you will introduce an infinite loop.
     */
    contentSourceActions: ContentSourceActions;
    /**
     * This function returns an object containing functions to create, update,
     * delete and publish documents in the content source matching the received
     * `srType` and `srcProjectId`.
     *
     * The `srcProjectId` is optional. However, if there are multiple
     * content source instances of the `srcType`, `undefined` will be returned
     *
     * All functions receive an optional `userContext`. If the `userContext`
     * is not specified, a default `userContext` for the current user and the
     * content source will be used.
     *
     * Calling these functions will trigger onDocument hooks. Therefore, do not
     * use these function to create, update, delete and publish the current
     * document, otherwise you will introduce an infinite loop.
     *
     * @param options
     * @param {string} options.srcType The content source type
     * @param {string} [options.srcProjectId] The content source project ID
     */
    getContentSourceActionsForSource: (options: {
        srcType: string;
        srcProjectId?: string;
    }) => ContentSourceActions | undefined;
    /**
     * Returns the `userContext` of the current user for the content source
     * specified by `srType`.
     *
     * The `userContext` can be used to create, update, delete and publish
     * documents on behalf of the current user.
     *
     * Generally you don't need to pass the `userContext` yourself as it will be
     * added automatically.
     *
     * @param {string} srcType The content source type
     */
    getUserContextForContentSourceType: (srcType: string) => User | undefined;
}

export interface ContentSourceActions {
    createDocument: (
        ...args: Parameters<ContentSourceInterface['createDocument']>
    ) => ReturnType<ContentSourceInterface['createDocument']>;
    /**
     * Creates a document from the provided `object` and `modelName`.
     * The `object` structure must conform to the {@link ContentObjectSpecification}.
     */
    createDocumentFromObject: (options: {
        object: ContentObjectSpecification;
        modelName: string;
        locale?: string;
    }) => ReturnType<ContentSourceInterface['createDocument']>;
    updateDocument: (
        ...args: Parameters<ContentSourceInterface['updateDocument']>
    ) => ReturnType<ContentSourceInterface['updateDocument']>;
    deleteDocument: (
        ...args: Parameters<ContentSourceInterface['deleteDocument']>
    ) => ReturnType<ContentSourceInterface['deleteDocument']>;
    publishDocuments: (
        ...args: Parameters<ContentSourceInterface['publishDocuments']>
    ) => ReturnType<ContentSourceInterface['publishDocuments']>;
}
