import {type MultipleMutationResult, type Mutation as RawMutation} from '@sanity/client'
import {type Path, type SanityDocument} from '@sanity/types'

import {type JsonArray, type JsonObject, type JsonValue} from './json.js'
import {type Mutation, type NodePatch, type Operation, type Transaction} from './mutations/index.js'
import {type RestrictedClient} from './runner/utils/createContextClient.js'

export type * from './json.js'

/**
 * @public
 */
export type AsyncIterableMigration = (
  documents: () => AsyncIterableIterator<SanityDocument>,
  context: MigrationContext,
) => AsyncGenerator<(Mutation | Transaction)[] | Mutation | Transaction>

/**
 * @public
 *
 * Main interface for a content migration definition
 * {@link https://www.sanity.io/docs/schema-and-content-migrations#af2be129ccd6}
 */
export interface Migration<Def extends MigrateDefinition = MigrateDefinition> {
  /**
   * An object of named helper functions corresponding to the primary schema type of the content you want to migrate.
   * You can also run these functions as async and return the migration instructions as promises if you need to fetch data from elsewhere.
   * If you want more control, `migrate` can also be an async iterable function that yields mutations or transactions.
   * {@link NodeMigration}
   * {@link AsyncIterableMigration}
   *
   */
  migrate: Def
  /**
   * A reader-friendly description of what the content migration does
   */
  title: string

  /**
   * An array of document types to run the content migration on. If you don’t define this, the migration type will target all document types.
   * Note: This reflects the document types your migration will be based on, not necessarily the documents you will modify.
   */
  documentTypes?: string[]
  /**
   * A simple GROQ-filter (doesn’t support joins) for documents you want to run the content migration on.
   * Note: instead of adding `_type == 'yourType'` to the filter, it's better to add its `_type` to `documentTypes`.
   * Note: `documentTypes` and `filter` are combined with AND. This means a document will only be included in the
   * migration if its `_type` matches any of the provided `documentTypes` AND it also matches the `filter` (if provided).
   */
  filter?: string
}

/**
 * @public
 *
 * Migration context. This will be passed to both async iterable migrations and node migration helper functions
 */
export interface MigrationContext {
  client: RestrictedClient
  dryRun: boolean
  filtered: {
    getDocument<T extends SanityDocument>(id: string): Promise<T | undefined>
    getDocuments<T extends SanityDocument>(ids: string[]): Promise<T[]>
  }
}

/**
 * @public
 *
 * Interface for `Migration['migrate']`. Either a NodeMigration or an AsyncIterableMigration
 * {@link NodeMigration}
 * {@link AsyncIterableMigration}
 */
export type MigrateDefinition = AsyncIterableMigration | NodeMigration

/**
 * Migration progress
 * @public
 */
export type MigrationProgress = {
  completedTransactions: MultipleMutationResult[]
  currentTransactions: (Mutation | Transaction)[]
  documents: number
  done?: boolean
  mutations: number
  pending: number
  queuedBatches: number
}

/**
 * API configuration for the migration runner
 * @public
 */
export interface APIConfig {
  apiVersion: 'vX' | `v${number}-${number}-${number}`
  dataset: string
  projectId: string
  token: string

  apiHost?: string
}

/**
 * API configuration for exports
 * @public
 */
export interface ExportAPIConfig extends APIConfig {
  documentTypes?: string[]
}

/**
 * @public
 *
 * Possible return values from a migration helper that runs on a document as a whole
 * Currently, this is only applies to {@link NodeMigration.document}
 */
export type DocumentMigrationReturnValue =
  | Mutation
  | Mutation[]
  | NodePatch
  | NodePatch[]
  | RawMutation
  | RawMutation[]

/**
 * @public
 *
 * Possible return values from a migration helper that runs on nodes within a document
 */
export type NodeMigrationReturnValue = DocumentMigrationReturnValue | Operation | Operation[]

/**
 * @public
 *
 * Node migration helper functions. As the migration is processing a document, it will visit each node in the document, depth-first, call the appropriate helper function for the node type and collect any mutations returned from it.
 */
export interface NodeMigration {
  /**
   * Helper function that will be called for each array in each document included in the migration
   * @param object - The object value currently being visited
   * @param path - The path to the node within the document. See `Path`
   * @param context - The {@link MigrationContext} instance
   */
  array?: <Node extends JsonArray>(
    node: Node,
    path: Path,
    context: MigrationContext,
  ) => NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void
  /**
   * Helper function that will be called for each boolean value in each document included in the migration
   * @param string - The string value currently being visited
   * @param path - The path to the node within the document. See `Path`
   * @param context - The {@link MigrationContext} instance
   */
  boolean?: <Node extends boolean>(
    node: Node,
    path: Path,
    context: MigrationContext,
  ) => NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void
  /**
   * Helper function for migrating a document as a whole
   * @param doc - The document currently being processed
   * @param context - The {@link MigrationContext} instance
   */
  document?: <Doc extends SanityDocument>(
    doc: Doc,
    context: MigrationContext,
  ) =>
    | DocumentMigrationReturnValue
    | Promise<DocumentMigrationReturnValue | Transaction>
    | Transaction
    | void
  /**
   * Helper function that will be called for each node in each document included in the migration
   * @param node - The node currently being visited
   * @param path - The path to the node within the document. See `Path`
   * @param context - The {@link MigrationContext} instance
   */
  node?: <Node extends JsonValue>(
    node: Node,
    path: Path,
    context: MigrationContext,
  ) => NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void
  /**
   * Helper function that will be called for each `null` value in each document included in the migration
   * @param string - The string value currently being visited
   * @param path - The path to the node within the document. See `Path`
   * @param context - The {@link MigrationContext} instance
   */
  null?: <Node extends null>(
    node: Node,
    path: Path,
    context: MigrationContext,
  ) => NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void
  /**
   * Helper function that will be called for each number in each document included in the migration
   * @param string - The string value currently being visited
   * @param path - The path to the node within the document. See `Path`
   * @param context - The {@link MigrationContext} instance
   */
  number?: <Node extends number>(
    node: Node,
    path: Path,
    context: MigrationContext,
  ) => NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void
  /**
   * Helper function that will be called for each object in each document included in the migration
   * @param object - The object value currently being visited
   * @param path - The path to the node within the document. See `Path`
   * @param context - The {@link MigrationContext} instance
   */
  object?: <Node extends JsonObject>(
    node: Node,
    path: Path,
    context: MigrationContext,
  ) => NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void
  /**
   * Helper function that will be called for each string in each document included in the migration
   * @param string - The string value currently being visited
   * @param path - The path to the node within the document. See `Path`
   * @param context - The {@link MigrationContext} instance
   */
  string?: <Node extends string>(
    node: Node,
    path: Path,
    context: MigrationContext,
  ) => NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void
}

export {type Path} from '@sanity/types'
