import path from 'node:path'

import {type CliCommandDefinition} from '@sanity/cli'
import {
  DEFAULT_MUTATION_CONCURRENCY,
  dryRun,
  MAX_MUTATION_CONCURRENCY,
  type Migration,
  type MigrationProgress,
  run,
} from '@sanity/migrate'
import {Table} from 'console-table-printer'
import {register} from 'esbuild-register/dist/node'
import {hideBin} from 'yargs/helpers'
import yargs from 'yargs/yargs'

import {debug} from '../../debug'
import {MIGRATIONS_DIRECTORY} from './constants'
import {resolveMigrations} from './listMigrationsCommand'
import {prettyFormat} from './prettyMutationFormatter'
import {isLoadableMigrationScript, resolveMigrationScript} from './utils/resolveMigrationScript'

const helpText = `
Options
  --no-dry-run By default the migration runs in dry mode. Pass this option to migrate dataset.
  --concurrency <concurrent> How many mutation requests to run in parallel. Must be between 1 and ${MAX_MUTATION_CONCURRENCY}. Default: ${DEFAULT_MUTATION_CONCURRENCY}.
  --no-progress Don't output progress. Useful if you want debug your migration script and see the output of console.log() statements.
  --dataset <dataset> Dataset to migrate. Defaults to the dataset configured in your Sanity CLI config.
  --project <project id> Project ID of the dataset to migrate. Defaults to the projectId configured in your Sanity CLI config.
  --no-confirm Skip the confirmation prompt before running the migration. Make sure you know what you're doing before using this flag.
  --from-export <export.tar.gz> Use a local dataset export as source for migration instead of calling the Sanity API. Note: this is only supported for dry runs.


Examples
  # dry run the migration
  sanity migration run <id>

  # execute the migration against a dataset
  sanity migration run <id> --no-dry-run --project xyz --dataset staging

  # execute the migration using a dataset export as the source
  sanity migration run <id>  --from-export=production.tar.gz --no-dry-run --projectId xyz --dataset staging
`

interface CreateFlags {
  ['dry-run']?: boolean
  concurrency?: number
  ['from-export']?: string
  progress?: boolean
  dataset?: string
  project?: string
  confirm?: boolean
}

function parseCliFlags(args: {argv?: string[]}) {
  return yargs(hideBin(args.argv || process.argv).slice(2))
    .options('dry-run', {type: 'boolean', default: true})
    .options('concurrency', {type: 'number', default: DEFAULT_MUTATION_CONCURRENCY})
    .options('progress', {type: 'boolean', default: true})
    .options('dataset', {type: 'string'})
    .options('from-export', {type: 'string'})
    .options('project', {type: 'string'})
    .options('confirm', {type: 'boolean', default: true}).argv
}

const runMigrationCommand: CliCommandDefinition<CreateFlags> = {
  name: 'run',
  group: 'migration',
  signature: 'ID',
  helpText,
  description: 'Run a migration against a dataset',
  // eslint-disable-next-line max-statements
  action: async (args, context) => {
    const {apiClient, output, prompt, chalk, workDir} = context
    const [id] = args.argsWithoutOptions
    const migrationsDirectoryPath = path.join(workDir, MIGRATIONS_DIRECTORY)

    const flags = await parseCliFlags(args)

    const fromExport = flags.fromExport
    const dry = flags.dryRun
    const dataset = flags.dataset
    const project = flags.project

    if ((dataset && !project) || (project && !dataset)) {
      throw new Error('If either --dataset or --project is provided, both must be provided')
    }

    if (!id) {
      output.error(chalk.red('Error: Migration ID must be provided'))
      const migrations = await resolveMigrations(workDir)
      const table = new Table({
        title: `Migrations found in project`,
        columns: [
          {name: 'id', title: 'ID', alignment: 'left'},
          {name: 'title', title: 'Title', alignment: 'left'},
        ],
      })

      migrations.forEach((definedMigration) => {
        table.addRow({id: definedMigration.id, title: definedMigration.migration.title})
      })
      table.printTable()
      output.print('\nRun `sanity migration run <ID>` to run a migration')

      return
    }

    if (!__DEV__) {
      register({
        target: `node${process.version.slice(1)}`,
      })
    }

    const candidates = resolveMigrationScript(workDir, id)
    const resolvedScripts = candidates.filter(isLoadableMigrationScript)

    if (resolvedScripts.length > 1) {
      // todo: consider prompt user about which one to run? note: it's likely a mistake if multiple files resolve to the same name
      throw new Error(
        `Found multiple migrations for "${id}" in ${chalk.cyan(migrationsDirectoryPath)}: \n - ${candidates
          .map((candidate) => path.relative(migrationsDirectoryPath, candidate.absolutePath))
          .join('\n - ')}`,
      )
    }

    const script = resolvedScripts[0]
    if (!script) {
      throw new Error(
        `No migration found for "${id}" in ${chalk.cyan(chalk.cyan(migrationsDirectoryPath))}. Make sure that the migration file exists and exports a valid migration as its default export.\n
 Tried the following files:\n - ${candidates
   .map((candidate) => path.relative(migrationsDirectoryPath, candidate.absolutePath))
   .join('\n - ')}`,
      )
    }

    const mod = script.mod
    if ('up' in mod || 'down' in mod) {
      // todo: consider adding support for up/down as separate named exports
      // For now, make sure we reserve the names for future use
      throw new Error(
        'Only "up" migrations are supported at this time, please use a default export',
      )
    }

    const migration: Migration = mod.default

    if (fromExport && !dry) {
      throw new Error('Can only dry run migrations from a dataset export file')
    }

    const concurrency = flags.concurrency
    if (concurrency !== undefined) {
      if (concurrency > MAX_MUTATION_CONCURRENCY) {
        throw new Error(
          `Concurrency exceeds the maximum allowed value of ${MAX_MUTATION_CONCURRENCY}`,
        )
      }

      if (concurrency === 0) {
        throw new Error(`Concurrency must be a positive number, got ${concurrency}`)
      }
    }

    const projectConfig = apiClient({
      requireUser: true,
      requireProject: true,
    }).config()

    const apiConfig = {
      dataset: dataset ?? projectConfig.dataset!,
      projectId: project ?? projectConfig.projectId!,
      apiHost: projectConfig.apiHost!,
      token: projectConfig.token!,
      apiVersion: 'v2024-01-29',
    } as const
    if (dry) {
      dryRunHandler()
      return
    }

    const response =
      flags.confirm &&
      (await prompt.single<boolean>({
        message: `This migration will run on the ${chalk.yellow(
          chalk.bold(apiConfig.dataset),
        )} dataset in ${chalk.yellow(chalk.bold(apiConfig.projectId))} project. Are you sure?`,
        type: 'confirm',
      }))

    if (response === false) {
      debug('User aborted migration')
      return
    }

    const spinner = output.spinner(`Running migration "${id}"`).start()
    await run({api: apiConfig, concurrency, onProgress: createProgress(spinner)}, migration)
    spinner.stop()

    function createProgress(progressSpinner: ReturnType<typeof output.spinner>) {
      return function onProgress(progress: MigrationProgress) {
        if (!flags.progress) {
          progressSpinner.stop()
          return
        }
        if (progress.done) {
          progressSpinner.text = `Migration "${id}" completed.

  Project id:  ${chalk.bold(apiConfig.projectId)}
  Dataset:     ${chalk.bold(apiConfig.dataset)}

  ${progress.documents} documents processed.
  ${progress.mutations} mutations generated.
  ${chalk.green(progress.completedTransactions.length)} transactions committed.`
          progressSpinner.stopAndPersist({symbol: chalk.green('✔')})
          return
        }

        ;[null, ...progress.currentTransactions].forEach((transaction) => {
          progressSpinner.text = `Running migration "${id}" ${dry ? 'in dry mode...' : '...'}

  Project id:     ${chalk.bold(apiConfig.projectId)}
  Dataset:        ${chalk.bold(apiConfig.dataset)}
  Document type:  ${chalk.bold(migration.documentTypes?.join(','))}

  ${progress.documents} documents processed…
  ${progress.mutations} mutations generated…
  ${chalk.blue(progress.pending)} requests pending…
  ${chalk.green(progress.completedTransactions.length)} transactions committed.

  ${
    transaction && !progress.done
      ? `» ${prettyFormat({chalk, subject: transaction, migration, indentSize: 2})}`
      : ''
  }`
        })
      }
    }

    async function dryRunHandler() {
      output.print(`Running migration "${id}" in dry mode`)

      if (fromExport) {
        output.print(`Using export ${chalk.cyan(fromExport)}`)
      }

      output.print()
      output.print(`Project id:  ${chalk.bold(apiConfig.projectId)}`)
      output.print(`Dataset:     ${chalk.bold(apiConfig.dataset)}`)

      for await (const mutation of dryRun({api: apiConfig, exportPath: fromExport}, migration)) {
        if (!mutation) continue
        output.print()
        output.print(
          prettyFormat({
            chalk,
            subject: mutation,
            migration,
          }),
        )
      }
    }
  },
}

export default runMigrationCommand
