/**
 * @module cli/commands/snapshot
 * @description Snapshot & time-travel CLI commands over the 8.0 Db API.
 *
 * - `snapshot <path>` — persist the current generation as a self-contained,
 *   hard-link snapshot directory (`brain.now().persist(path)`).
 * - `restore <path>` — replace the store's ENTIRE state from a snapshot
 *   (`brain.restore(path, { confirm: true })`, after an interactive confirm).
 * - `history` — the reified transaction log: one line per committed
 *   `transact()` batch (generation, commit time, metadata).
 * - `generation` — the store's current generation watermark.
 */

import chalk from 'chalk'
import ora from 'ora'
import inquirer from 'inquirer'
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
import { Brainy } from '../../brainy.js'

/**
 * @description Shared CLI flags every snapshot command accepts (`--verbose`,
 * `--json`, `--pretty`), mirroring the other command modules.
 */
interface SnapshotCliOptions {
  verbose?: boolean
  json?: boolean
  pretty?: boolean
}

let brainyInstance: Brainy | null = null

const getBrainy = (): Brainy => {
  if (!brainyInstance) {
    brainyInstance = new Brainy()
  }
  return brainyInstance
}

const formatOutput = (data: unknown, options: SnapshotCliOptions): void => {
  if (options.json) {
    console.log(options.pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data))
  }
}

/**
 * @description The `snapshot`/`restore`/`history`/`generation` command
 * handlers registered in `src/cli/index.ts`.
 */
export const snapshotCommands = {
  /**
   * @description Persist the current generation as a self-contained snapshot
   * directory. Instant on same-device targets (hard links); the result opens
   * with `Brainy.load(path)` or restores wholesale via `brainy restore`.
   * @param path - Target directory (created; must be empty or absent).
   * @param options - Shared CLI flags.
   */
  async snapshot(path: string, options: SnapshotCliOptions) {
    let spinner: ReturnType<typeof ora> | null = null
    try {
      const brain = getBrainy()
      await brain.init()

      const target = resolve(path)
      spinner = ora(`Persisting snapshot to ${chalk.cyan(target)}...`).start()

      const startTime = Date.now()
      const db = brain.now()
      try {
        await db.persist(target)
      } finally {
        await db.release()
      }
      const elapsed = Date.now() - startTime

      spinner.succeed(
        `Snapshot persisted: ${chalk.green(target)} ${chalk.dim(`(generation ${db.generation}, ${elapsed}ms)`)}`
      )
      console.log(`
${chalk.cyan('Snapshot:')}
  ${chalk.dim('Path:')} ${target}
  ${chalk.dim('Generation:')} ${db.generation}
  ${chalk.dim('Open with:')} Brainy.load('${target}')
  ${chalk.dim('Restore with:')} brainy restore ${target}
      `.trim())

      formatOutput({ path: target, generation: db.generation, time: elapsed }, options)

      // close() releases the writer lock and indexes, but global timers
      // (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
      // alive. CLI commands are one-shot — exit explicitly.
      await brain.close()
      process.exit(0)
    } catch (error: any) {
      if (spinner) spinner.fail('Snapshot failed')
      console.error(chalk.red('Error:'), error.message)
      if (options.verbose) console.error(error)
      process.exit(1)
    }
  },

  /**
   * @description Replace the store's ENTIRE current state from a snapshot
   * directory. Destructive — asks for interactive confirmation first
   * (skipped with `--force`).
   * @param path - Snapshot directory produced by `brainy snapshot <path>`.
   * @param options - Shared CLI flags plus `--force`.
   */
  async restore(path: string, options: SnapshotCliOptions & { force?: boolean }) {
    let spinner: ReturnType<typeof ora> | null = null
    try {
      const target = resolve(path)
      if (!existsSync(target)) {
        throw new Error(`Snapshot directory not found: ${target}`)
      }

      if (!options.force) {
        const { confirm } = await inquirer.prompt([
          {
            type: 'confirm',
            name: 'confirm',
            message: `Replace the store's ENTIRE current state with the snapshot at '${target}'? This cannot be undone.`,
            default: false
          }
        ])
        if (!confirm) {
          console.log(chalk.yellow('Restore cancelled'))
          process.exit(0)
        }
      }

      const brain = getBrainy()
      await brain.init()

      spinner = ora(`Restoring from ${chalk.cyan(target)}...`).start()
      const startTime = Date.now()
      await brain.restore(target, { confirm: true })
      const elapsed = Date.now() - startTime

      spinner.succeed(
        `Restored from ${chalk.green(target)} ${chalk.dim(`(now at generation ${brain.generation()}, ${elapsed}ms)`)}`
      )

      formatOutput({ path: target, generation: brain.generation(), time: elapsed }, options)

      // One-shot command — see snapshot() for why the explicit exit.
      await brain.close()
      process.exit(0)
    } catch (error: any) {
      if (spinner) spinner.fail('Restore failed')
      console.error(chalk.red('Error:'), error.message)
      if (options.verbose) console.error(error)
      process.exit(1)
    }
  },

  /**
   * @description Show the reified transaction log, newest first: one entry
   * per committed `transact()` batch with its generation, commit time, and
   * the `meta` the transaction was submitted with.
   * @param options - Shared CLI flags plus `--limit`.
   */
  async history(options: SnapshotCliOptions & { limit?: string }) {
    try {
      const brain = getBrainy()
      await brain.init()

      const limit = options.limit ? parseInt(options.limit, 10) : 10
      const entries = await brain.transactionLog({ limit })

      if (entries.length === 0) {
        console.log(chalk.yellow('\nNo committed transactions yet (the log records transact() batches).\n'))
        formatOutput([], options)
        await brain.close()
        process.exit(0)
      }

      console.log(chalk.cyan(`\nTransaction Log (last ${entries.length}):\n`))
      for (const entry of entries) {
        const age = formatAge(Date.now() - entry.timestamp)
        const meta = entry.meta ? ` ${chalk.dim(JSON.stringify(entry.meta))}` : ''
        console.log(
          `${chalk.yellow(`g${entry.generation}`)} ` +
          `${chalk.dim(new Date(entry.timestamp).toISOString())} ` +
          `${chalk.dim(`(${age} ago)`)}` +
          meta
        )
      }
      console.log()

      formatOutput(entries, options)

      // One-shot command — see snapshot() for why the explicit exit.
      await brain.close()
      process.exit(0)
    } catch (error: any) {
      console.error(chalk.red('Error:'), error.message)
      if (options.verbose) console.error(error)
      process.exit(1)
    }
  },

  /**
   * @description Print the store's current generation watermark (bumped once
   * per committed `transact()` batch and once per single-operation write
   * batch).
   * @param options - Shared CLI flags.
   */
  async generation(options: SnapshotCliOptions) {
    try {
      const brain = getBrainy()
      await brain.init()

      const generation = brain.generation()
      console.log(`${chalk.cyan('Generation:')} ${chalk.green(String(generation))}`)

      formatOutput({ generation }, options)

      // One-shot command — see snapshot() for why the explicit exit.
      await brain.close()
      process.exit(0)
    } catch (error: any) {
      console.error(chalk.red('Error:'), error.message)
      if (options.verbose) console.error(error)
      process.exit(1)
    }
  }
}

/**
 * @description Format a millisecond duration as a compact age string
 * (`3d` / `5h` / `12m` / `42s`).
 * @param ms - Elapsed milliseconds.
 * @returns The compact age string.
 */
function formatAge(ms: number): string {
  const seconds = Math.floor(ms / 1000)
  const minutes = Math.floor(seconds / 60)
  const hours = Math.floor(minutes / 60)
  const days = Math.floor(hours / 24)

  if (days > 0) return `${days}d`
  if (hours > 0) return `${hours}h`
  if (minutes > 0) return `${minutes}m`
  return `${seconds}s`
}
