import type { ChildProcessWithoutNullStreams } from 'child_process';
import type { Model } from './models';
import type { FieldPath } from './model-fields';
import type { Logger } from './logger';
import type { ScheduledAction, ScheduledActionActionType } from './content-source-scheduled-action';
import type { Document, Asset, TypeDocument, TypeAsset, DocumentStatus } from './content-source-document';
import type { UpdateOperation, UpdateOperationField } from './content-source-operation';
import type { DocumentVersion, DocumentVersionWithDocument } from './content-source-document-versions';
import type { PluginRef } from './content-engine';

/**
 * A class implementing the `ContentSourceInterface` allows Stackbit to
 * communicate with the underlying content source.
 *
 * The instantiation of the content source is done inside the stackbit.config.ts
 * file. The content source may define arbitrary construction options needed to
 * connect and communicate with the underlying content source. For example,
 * construction options may include project ID, API keys, etc.
 *
 * The Content Source Interface has four generic types:
 * - `SchemaContext`: A generic type the content source instance may use to
 *   store custom data in the {@link Schema.context} property. Stackbit will
 *   cache the custom data with the rest of the {@link Schema} data. The cached
 *   {@link Schema} can be retrieved using the {@link InitOptions.cache.getSchema}
 *   method provided to the {@link init} method.
 * - `UserContext`: A generic type defining user-specific context properties
 *   like a username, an email address and an OAuth accessToken. To use the
 *   `UserContext`, it is required to create an OAuth integration between
 *   Stackbit and the underlying content. Please contact Stackbit support for
 *   more info.
 * - `DocumentContext`: A generic type the content source instance may use to
 *   store custom data in the {@link Document.context} property. Stackbit will
 *   cache the custom data with the rest of the {@link Document} data. The cached
 *   {@link Document} can be retrieved using the {@link InitOptions.cache.getDocuments}
 *   and {@link InitOptions.cache.getDocumentById} methods provided to the
 *   {@link init} method.
 * - `AssetContext`: A generic type the content source instance may use to store
 *   custom data in the {@link Asset.context} property. Stackbit will cache the
 *   custom data with the rest of the {@link Asset} data. The cached {@link Asset}
 *   can be retrieved using the {@link InitOptions.cache.getAssets} and
 *   {@link InitOptions.cache.getAssetById} methods provided to the {@link init}
 *   method.
 */
export interface ContentSourceInterface<
    UserContext = unknown,
    SchemaContext = unknown,
    DocumentContext = unknown,
    AssetContext = unknown,
    ModelContext = unknown
> {
    /**
     * The `getVersion` method should return the {@link Version} specifying
     * the ContentSourceInterface version the content source module implements,
     * and the version of the content source module, possibly matching the
     * version in the `package.json`.
     *
     * You can use the {@link import('./utils').getVersion} utility method to
     * read the content source module version from the package.json and get
     * the current content source interface version:
     * @example
     * async getVersion(): Promise<Version> {
     *     return getVersion({
     *         packageJsonPath: path.join(__dirname, '../package.json')
     *     });
     * }
     */
    getVersion(): Promise<Version>;

    /**
     * The `getContentSourceType` method should return the type of the content
     * source. The content source type must be unique among other content
     * sources used by the same project.
     */
    getContentSourceType(): string;

    /**
     * The `getProjectId` method should return the unique identifier of the
     * content source instance. This identified must be unique among other
     * content sources instances of the same content source type.
     */
    getProjectId(): string;

    /**
     * @deprecated
     *
     * The `getProjectEnvironment` method should return a string representing
     * additional segmentation of a particular content source instance. This
     * might be useful when a content source supports projects with multiple
     * environments.
     */
    getProjectEnvironment(): string;

    /**
     * The `getProjectManageUrl` method should return the URL address of the
     * underlying content source's web application. Stackbit uses this URL to
     * show links to the content source web application.
     */
    getProjectManageUrl(): string;

    /**
     * The `init` method should initialize the content source instance.
     *
     * This is a good place to create and initialize API clients using the
     * options passed to the constructor, fetch and cache meta-data that does
     * not frequently change, such as users, locales, and other configuration.
     *
     * The content source instance should remain stateless and should not store
     * models, documents, or assets. After initializing the content source,
     * Stackbit will fetch and cache the needed data using the methods like
     * {@link getSchema}, {@link getDocuments}, and {@link getAssets}. When
     * the content source instance needs to access the cached data, it may call
     * functions with matching names passed to this method inside the
     * `options.cache` property. This paradigm ensures that Stackbit and the
     * content source instance work on the same data.
     *
     * @param {InitOptions} options
     */
    init(options: InitOptions<SchemaContext, DocumentContext, AssetContext, ModelContext>): Promise<void>;

    /**
     * The `reset` method should reset the internal state, clean up any instance
     * variables, and cached meta-data fetched in the `init` method and
     * re-fetch it again.
     */
    reset(): Promise<void>;

    /**
     * This function should destroy the content source instance by clearing any
     * listeners and observers that might keep the instance from being garbage
     * collected.
     *
     * This function is called when Stackbit reloads the stackbit.config.ts file.
     * For example, when the config file is changed. Once the config file is
     * reloaded, new instances of content sources are created and initialized.
     * And the old instances are destroyed.
     */
    destroy(): Promise<void>;

    /**
     * Stackbit calls the `onWebhook` method when the underlying content
     * source calls a previously configured webhook.
     *
     * The content source module may set up webhooks between the underlying
     * content source and Stackbit using the {@link InitOptions.webhookUrl}
     * passed to the {@link init} method. When the underlying content source
     * calls the webhook, Stackbit passes the webhook request to this method
     * with its data and request headers. Webhooks can be used to trigger the
     * {@link InitOptions.cache.updateContent}, and the
     * {@link InitOptions.cache.invalidateSchema} callbacks passed to the
     * {@link init} method.
     *
     * This method is not called in local development (when
     * {@link InitOptions.localDev} is `true`).
     * To debug webhooks locally, you need to create a public URL that
     * forwards external webhooks to stackbit dev's internal port: `localhost:8090`.
     * You can use a tool like 'ngrok' and run `ngrok http 8090` to create a
     * public URL that forwards webhooks to stackbit dev. Ngrok will print the
     * public URL it created (e.g., https://xyz.ngrok.io). Then use this URL
     * when running stackbit dev:
     * `stackbit dev --log-level=debug --csi-webhook-url=https://xyz.ngrok.io/_stackbit/onWebhook`
     *
     * @param data
     * @param {any} data.data
     * @param {Record<string, string>} data.headers
     */
    onWebhook?(data: { data: unknown; headers: Record<string, string> }): void;

    /**
     * Stackbit calls the `onFilesChange` method when it detects changes in
     * the project files. This method is optional and should be used when the
     * content source stores its schema or content in files within the project
     * repository. Therefore, if your content source relies on files within
     * the project repository, you don't need to set up any file-watching
     * processes. Stackbit will watch the files for you and call this method
     * when the files are changed.
     *
     * When this method is called, the content source module should check if the
     * changed files represent a document or a model and return a matching result.
     *
     * @param options
     * @param {string[]} options.updatedFiles
     *   A list of updated files. The file paths are relative to the project
     *   root folder.
     */
    onFilesChange?({
        updatedFiles
    }: {
        updatedFiles: string[];
    }): Promise<{ invalidateSchema?: boolean; contentChanges?: ContentChanges<DocumentContext, AssetContext> }>;

    /**
     * Stackbit calls the `startWatchingContentUpdates` method after it has
     * initialized the content source instance and loaded its schema, documents
     * and assets, and is ready to receive the content and the schema updates.
     *
     * This method may also be called after calling the
     * {@link stopWatchingContentUpdates} and as soon as Stackbit app becomes
     * active again.
     *
     * This method should start watching for content and schema updates using
     * any available content synchronization mechanism. For example, polling the
     * content source's synchronization API or setting up a server-to-server
     * listeners.
     *
     * When the content source instance detects a change in the content or the
     * schema, it should call the `updateContent` or the `invalidateSchema`
     * callbacks provided inside the `cache` object to the {@link init} method.
     *
     * Note: if you use webhooks as your content synchronization mechanism, you
     * don't need to implement this method.
     *
     * Note: The content source module should remain stateless in respect to the
     * fetched content and schema. It should not store models, documents, or
     * assets. If you need to access models, documents, or assets previously
     * returned from `getSchema`, `getDocuments`, or `getAssets` methods, use
     * the matching functions provided to the {@link init} method inside the
     * `options.cache` property.
     *
     * @param {object} options
     *   The 'options' parameter is deprecated, use methods passed to the
     *   {@link init} method.
     */
    startWatchingContentUpdates?(): void;

    /**
     * Stackbit calls the `stopWatchingContentUpdates` method after Stackbit app
     * has not been used for a while.
     *
     * You can use this method to stop watching for content and schema updates,
     * and stop calling the `updateContent` and the `invalidateSchema` callbacks
     * provided to the {@link init} method until the
     * {@link startWatchingContentUpdates} method is called again.
     *
     * This method may be useful when the content synchronization mechanism uses
     * rapid polling requests that may consume the monthly API request limit of
     * a headless CMS.
     *
     * Note: if you use webhooks as your content synchronization mechanism, you
     * don't need to implement this method.
     */
    stopWatchingContentUpdates?(): void;

    /**
     * The `getSchema` method should fetch all the content models from the
     * underlying content source and convert them to an array of Stackbit
     * [Models]{@link Model}.
     *
     * Optionally, if the underlying content source supports locales, this
     * method may fetch the available locales and convert them to an array of
     * Stackbit [Locales]{@link Locale}.
     *
     * The returned object should be of type {@link Schema} having the
     * following interface:
     * ```typescript
     * interface Schema {
     *     models: Model[],
     *     locales?: Locale[]
     * }
     * ```
     */
    getSchema(): Promise<Schema<SchemaContext, ModelContext>>;

    /**
     * The `getDocuments` method should fetch all the documents from the
     * underlying content source and convert them to an array of Stackbit
     * [Documents]{@link Document}. You can define the `DocumentContext` generic
     * type to set the {@link Document.context} to a custom data that your
     * content source may need. Stackbit will cache the documents with the
     * context data.
     */
    getDocuments(options?: {
        syncContext?: unknown;
    }): Promise<Document<DocumentContext>[] | { documents: Document<DocumentContext>[]; syncContext?: unknown }>;

    /**
     * The `getAssets` method should fetch all the assets from the underlying
     * content source and convert them to an array Stackbit [Assets]{@link Asset}.
     * You can define the `AssetContext` generic type to set the
     * {@link Asset.context} to a custom data that your content source may need.
     * Stackbit will cache the assets with the context data.
     */
    getAssets(options?: {
        syncContext?: unknown;
    }): Promise<Asset<AssetContext>[] | { assets: Asset<AssetContext>[]; syncContext?: unknown }>;

    /**
     * The `hasAccess` method should check if the current user has read/write
     * access to the content source.
     *
     * @param {Object} options
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source. Please contact Stackbit support if you want to
     *   integrate your content source's OAuth flow with Stackbit.
     */
    hasAccess(options: {
        userContext?: User<UserContext>;
    }): Promise<{ hasConnection: boolean; hasPermissions: boolean }>;

    /**
     * The `createDocument` method should create a document in the underlying
     * content source and return the id of the created document.
     *
     * @param {Object} options
     * @param {Record<string, UpdateOperationField>} options.updateOperationFields
     *   A map of update operation fields by field names.
     * @param {Model} options.model
     *   A model representing the document to be created.
     * @param {string} [options.locale]
     *   A locale id for the document to be created.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    createDocument(options: {
        updateOperationFields: Record<string, UpdateOperationField>;
        model: Model<ModelContext>;
        locale?: string;
        defaultLocaleDocumentId?: string;
        userContext?: User<UserContext>;
    }): Promise<{ documentId: string }>;

    /**
     * The `updateDocument()` method should update a document in the underlying
     * content source and return the updated document as a Stackbit Document.
     *
     * @param {Object} options
     * @param {Document} options.document
     *   A document to be updated.
     * @param {UpdateOperation[]} options.operations
     *   An array of update operations.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    updateDocument(options: {
        document: Document<DocumentContext>;
        operations: UpdateOperation[];
        userContext?: User<UserContext>;
    }): Promise<void>;

    /**
     * The `deleteDocument` method should delete a document from the underlying
     * content source.
     *
     * @param {Object} options
     * @param {Document} options.document
     *   A document to be deleted.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    deleteDocument(options: { document: Document<DocumentContext>; userContext?: User<UserContext> }): Promise<void>;

    /**
     * The `archiveDocument` method should archive a document from the underlying
     * content source.
     *
     * @param {Object} options
     * @param {Document} options.document
     *   A document to be archived.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    archiveDocument?(options: { document: Document<DocumentContext>; userContext?: User<UserContext> }): Promise<void>;

    /**
     * The `unarchiveDocument` method should unarchive a document from the underlying
     * content source.
     *
     * @param {Object} options
     * @param {Document} options.document
     *   A document to be unarchived.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    unarchiveDocument?(options: {
        document: Document<DocumentContext>;
        userContext?: User<UserContext>;
    }): Promise<void>;

    /**
     * The `getScheduledActions` method should fetch all the scheduled actions (publish | unpublish) from the
     * underlying content source and convert them to an array of Stackbit
     * [ScheduledAction]{@link ScheduledAction}.
     */
    getScheduledActions?(): Promise<ScheduledAction[]>;
    /**
     * The `createScheduledAction` method should create a scheduled action in the underlying
     * content source.
     *
     * @param {Object} options
     * @param {string} options.name Name of the scheduled action
     * @param {ScheduledActionActionType} options.action type of Action (publish | unpublish)
     * @param {string[]} options.documentIds A list of document ids to schedule
     * @param {string} options.executeAt The date to execute the action in ISO format
     * @param {UserContext} options.userContext
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    createScheduledAction?(options: {
        name: string;
        action: ScheduledActionActionType;
        documentIds: string[];
        executeAt: string;
        userContext?: User<UserContext>;
    }): Promise<{ newScheduledActionId: string }>;
    /**
     * The `cancelScheduledAction` method should delete a scheduled action from the underlying
     * content source.
     *
     * @param {Object} options
     * @param {string} options.scheduledActionId The id of the schedule to cancel
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    cancelScheduledAction?(options: {
        scheduledActionId: string;
        userContext?: User<UserContext>;
    }): Promise<{ cancelledScheduledActionId: string }>;
    /**
     * The `updateScheduledAction` method should edit a scheduled action in the underlying
     * content source.
     *
     * @param {Object} options
     * @param {string} options.scheduledActionId The id of the schedule to edit
     * @param {string} [options.name] Name of the scheduled action
     * @param {string[]} [options.documentIds] A list of document ids to schedule
     * @param {string} [options.executeAt] The date to execute the action in ISO format
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    updateScheduledAction?(options: {
        scheduledActionId: string;
        name?: string;
        documentIds?: string[];
        executeAt?: string;
        userContext?: User<UserContext>;
    }): Promise<{ updatedScheduledActionId: string }>;

    /**
     * The `uploadAsset` method should upload an asset to the underlying
     * content source and return the upload asset as a Stackbit Asset.
     *
     * @param {Object} options
     * @param {string} options.fileName
     *   The fileName of the asset.
     * @param {string} options.mimeType
     *   The mimeType of the asset.
     * @param {string} [options.locale]
     *   The locale id for the asset.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    uploadAsset(options: {
        url?: string;
        base64?: string;
        fileName: string;
        mimeType: string;
        locale?: string;
        userContext?: User<UserContext>;
    }): Promise<Asset<AssetContext>>;

    /**
     * The `updateAsset()` method should update an asset in the underlying
     * content source
     *
     * @param {Object} options
     * @param {Asset} options.asset
     *   An asset to be updated.
     * @param {UpdateOperation[]} options.operations
     *   An array of update operations.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    updateAsset?(options: {
        asset: Asset<AssetContext>;
        operations: UpdateOperation[];
        userContext?: User<UserContext>;
    }): Promise<void>;

    /**
     * The `validateDocuments` method should validate documents according to
     * the underlying content source validation rules and return an array of
     * {@link ValidationError} if the documents do not pass validation.
     */
    validateDocuments(options: {
        documents: Document<DocumentContext>[];
        assets: Asset<AssetContext>[];
        locale?: string;
        userContext?: User<UserContext>;
    }): Promise<{ errors: ValidationError[] }>;

    /**
     * The `publishDocuments` method should publish documents in the
     * underlying content source.
     *
     * @param {Object} options
     * @param {Document[]} options.documents
     *   The array of documents to be published.
     * @param {Asset[]} options.assets
     *   The array of assets to be published.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    publishDocuments(options: {
        documents: Document<DocumentContext>[];
        assets: Asset<AssetContext>[];
        userContext?: User<UserContext>;
    }): Promise<void>;

    /**
     * The `unpublishDocuments` method should unpublish documents in the
     * underlying content source.
     *
     * @param {Object} options
     * @param {Document[]} options.documents
     *   The array of documents to be unpublished.
     * @param {Asset[]} options.assets
     *   The array of assets to be unpublished.
     * @param {UserContext} [options.userContext]
     *   User properties provided by the OAuth flow between Stackbit and the
     *   underlying content source. The type of the `userContext` is defined by
     *   the `UserContext` generic type. The `userContext` is passed only if
     *   there is an OAuth integration between Stackbit and the underlying
     *   content source.
     */
    unpublishDocuments?(options: {
        documents: Document<DocumentContext>[];
        assets: Asset<AssetContext>[];
        userContext?: User<UserContext>;
    }): Promise<void>;

    /**
     * Returns a list of plugins to be used by content engine.
     */
    getContentEngineConfig?(): ContentEngineConfig;

    /**
     * Get content versions of the document. Result object might optionally have document with fields
     * if CMS can return them in the same request
     * @param {Object} options
     * @param {string} options.documentId
     */
    getDocumentVersions?(options: { documentId: string }): Promise<{ versions: DocumentVersion[] }>;

    /**
     * Get content version of the document with fields
     * @param {Object} options
     * @param {string} options.documentId
     * @param {string} options.versionId
     */
    getDocumentForVersion?(options: {
        documentId: string;
        versionId: string;
    }): Promise<{ version: DocumentVersionWithDocument }>;
}

export type ReturnTypeOfCSIMethod<Method extends keyof ContentSourceInterface> = ReturnType<
    NonNullable<ContentSourceInterface[Method]>
>;
export type ParametersOfCSIMethod<Method extends keyof ContentSourceInterface> = Parameters<
    NonNullable<ContentSourceInterface[Method]>
>;

/**
 * The config for a linked content engine connector
 */

export interface ContentEngineConfig {
    /**
     * The name of the connector. Used to handle selective syncing
     */
    connector: string;
    /**
     * Plugins to add to content engine. Ensure these are also dependencies of the package.
     * @see https://github.com/netlify/content-engine#public-content-engine-api
     */
    plugins: PluginRef[] | PluginRef;
}

export interface Version {
    /**
     * The ContentSourceInterface version the content source module implements.
     * Use the `getInterfaceVersion()` utility method to get the current version.
     */
    interfaceVersion: string;
    /**
     * The current version of the Content Source module. This should be the
     * version stored in the package.json of the content source.
     */
    contentSourceVersion: string;
}

export interface InitOptions<
    SchemaContext = unknown,
    DocumentContext = unknown,
    AssetContext = unknown,
    ModelContext = unknown
> {
    /**
     * @type Logger
     * A logger object used to log messages to the terminal console. Messages
     * logged with this logger will be shown in the console when running
     * `stackbit dev`. Use the `--log-level` argument to configure the log
     * levels printed by `stackbit dev`.
     *
     * Use this logger to debug the content source when working locally with
     * `stackbit dev`
     *
     * @example:
     * stackbit dev --log-level debug
     **/
    logger: Logger;
    /**
     * @type Logger
     * A logger object used to log messages to the "log" panel in the Stackbit
     * client application.
     *
     * Use this logger to print important messages to the user using the
     * Stackbit UI.
     */
    userLogger: Logger;
    /**
     * @type boolean
     * A boolean flag indicating if the content source is running in local
     * development mode using the `stackbit dev` command (true), or if it is
     * running in Stackbit cloud project (false).
     */
    localDev: boolean;
    stackbitConfigFilePath: string;
    userCommandSpawner?: UserCommandSpawner;
    /**
     * @type string
     * A string representing a Stackbit webhook URL that the content source
     * module may use to create webhooks between the content source and Stackbit.
     *
     * Webhooks need to be set once. Use predefined webhook names to check if
     * the webhook was already created. This parameter is empty when `localDev`
     * is `true`.
     */
    webhookUrl?: string;
    devAppRestartNeeded?: () => void;
    /**
     * @type Cache
     * A Stackbit managed cache for documents, assets and models returned by the
     * content source. Use cache methods to get, update, delete and invalidate
     * data stored in the cache.
     */
    cache: Cache<SchemaContext, DocumentContext, AssetContext, ModelContext>;
    git: GitServiceInterface;
    runCommand: CommandRunner;
}

export type UserSSOProfile = {
    idpId: string;
    firstName?: string;
    lastName?: string;
    emails?: {
        value: string;
        type?: string;
        primary?: boolean;
    }[];
    attributes?: Record<string, any>;
};

export type User<UserContext = unknown> = {
    name: string;
    email: string;
    role?: string;
    sso?: UserSSOProfile;
} & UserContext;

export type UserCommandSpawner = (options: SpawnUserCommandOptions) => ChildProcessWithoutNullStreams;

export interface SpawnUserCommandOptions {
    command: string;
    args?: string[];
    cwd?: string;
    env?: NodeJS.ProcessEnv;
}

export type RunResult = { stdout: string; stderr: string; exitCode?: number; err?: Error };

export type CommandRunner = (
    command: string,
    args?: string[],
    options?: { cwd?: string; shell?: boolean; env?: NodeJS.ProcessEnv; logger?: Logger }
) => Promise<RunResult>;

export interface GitFileCommitDescriptor {
    filePath: string;
    description: string;
}

export interface GitAuthor {
    name?: string;
    email: string;
}

export interface GitCommitLogEntry {
    author: string;
    timestamp: Date;
    commitHash: string;
    changes: {
        status: Extract<DocumentStatus, 'modified' | 'added' | 'deleted'>;
        filePath: string;
        fromFilePath?: string;
    }[];
}

// Git service listeners
export type GitServicePullListener = (options: { branch: string; updatedFiles: string[] }) => Promise<void>;
export type GitServicePushListener = () => Promise<void>;

export type GitFileStatus = {
    filePath: string;
    status: 'modified' | 'added' | 'deleted';
};

export interface GitServiceInterface {
    getRepoUrl(): string;
    getRepoBranch(): string;
    getRepoPublishBranch(): string;
    getRepoDir(): string;
    setRepoUrl(repoUrl: string): void;
    setRepoBranch(repoBranch: string): void;
    setRepoPublishBranch(publishBranch: string): void;
    commitAndPush: (author: GitAuthor, files: GitFileCommitDescriptor[]) => Promise<void>;
    publish(author: GitAuthor, filePaths?: string[]): Promise<void>;
    commitLog(): Promise<GitCommitLogEntry[]>;
    diff(): Promise<string[]>;
    addPullListener(listener: GitServicePullListener): void;
    removePullListener(listener: GitServicePullListener): void;
    addPushListener(listener: GitServicePushListener): void;
    removePushListener(listener: GitServicePushListener): void;
    diffFilesWithFetchHead(): Promise<GitFileStatus[]>;
    pullFilesFromFetchHead(files: GitFileStatus[]): Promise<void>;
    createShadowRepo(): Promise<void>;
}

/**
 * A Stackbit managed cache for documents, assets and models returned by the
 * content source module. Use cache methods to get, update, delete and invalidate
 * data stored in the cache.
 */
export interface Cache<
    SchemaContext = unknown,
    DocumentContext = unknown,
    AssetContext = unknown,
    ModelContext = unknown
> {
    /**
     * Returns the documents that were originally returned from the content
     * source's [getDocuments]{@link ContentSourceInterface#getDocuments} method.
     * @return Document[]
     */
    getDocuments: () => Document<DocumentContext>[];
    /**
     * Returns the assets that were originally returned from the content
     * source's [getAssets]{@link ContentSourceInterface#getAssets} method.
     * @return Asset[]
     */
    getAssets: () => Asset<AssetContext>[];
    /**
     * Returns the scheduled actions that were originally returned from the content
     * source's [getScheduledActions]{@link ContentSourceInterface#getScheduledActions} method.
     * @return ScheduledAction[]
     */
    getScheduledActions: () => ScheduledAction[];
    /**
     * Returns the scheduled actions that contain the passed documentId that were originally returned from the content
     * source's [getScheduledActions]{@link ContentSourceInterface#getScheduledActions} method.
     * @param {string} documentId filter only scheduled for a specific document
     * @return ScheduledAction[]
     */
    getScheduledActionsForDocumentId: (documentId: string) => ScheduledAction[];
    /**
     * Returns the schema that were originally returned from the content
     * source's [getSchema]{@link ContentSourceInterface#getSchema} method.
     *
     * @return Schema
     */
    getSchema: () => Schema<SchemaContext, ModelContext>;
    /**
     * Returns a document by [Document.id]{@link Document.id}.
     * @param options
     * @param {string} options.documentId
     * @return Document | undefined
     */
    getDocumentById: (documentId: string) => Document<DocumentContext> | undefined;
    /**
     * Returns an asset by [Asset.id]{@link Asset.id}.
     * @param options
     * @param {string} options.assetId
     * @return Asset | undefined
     */
    getAssetById: (assetId: string) => Asset<AssetContext> | undefined;
    /**
     * Returns a model by [Model.name]{@link Model.name}.
     * @param {string} modelName
     * @return Model | undefined
     */
    getModelByName: (modelName: string) => Model<ModelContext> | undefined;

    getSyncContext: () => {
        documentsSyncContext?: unknown;
        assetsSyncContext?: unknown;
    };
    clearSyncContext: (options?: {
        clearDocumentsSyncContext?: boolean;
        clearAssetsSyncContext?: boolean;
    }) => Promise<void>;
    requestSync: (options?: { clearDocumentsSyncContext?: boolean; clearAssetsSyncContext?: boolean }) => Promise<void>;
    updateContent: (contentChanges: ContentChanges<DocumentContext, AssetContext>) => Promise<void>;
    invalidateSchema: () => Promise<void>;

    /** Experimental, local-dev mode only, allows getting arbitrary key-value from file-based cache */
    get: (key: string) => Promise<unknown>;
    /** Experimental, local-dev mode only, allows setting arbitrary key-value into file-based cache */
    set: (key: string, value: unknown) => Promise<void>;
    /** Experimental, local-dev mode only, allows removing arbitrary key-value file-based cache */
    remove: (key: string) => Promise<void>;
}

export interface ContentChanges<DocumentContext = unknown, AssetContext = unknown> {
    documents?: Document<DocumentContext>[];
    assets?: Asset<AssetContext>[];
    scheduledActions?: ScheduledAction[];
    deletedDocumentIds?: string[];
    deletedAssetIds?: string[];
    deletedScheduledActionIds?: string[];
}

// backward compatibility
export type ContentChangeEvent<DocumentContext = unknown, AssetContext = unknown> = Required<
    ContentChanges<DocumentContext, AssetContext>
>;

export interface Schema<SchemaContext = unknown, ModelContext = unknown> {
    models: Model<ModelContext>[];
    locales?: Locale[];
    context: SchemaContext;
}

export type SchemaWithSource = Schema & { srcType: string; srcProjectId: string };

export type ModelMap = Record<string, Model>;

export interface Locale {
    code: string;
    default?: boolean;
}

export type ValidationError = {
    message: string;
    objectType: TypeDocument | TypeAsset;
    objectId: string;
    fieldPath: FieldPath;
    isUniqueValidation?: boolean;
};
