/**
 * @module cli/commands/storage
 * @description Storage management CLI commands for the 8.0 storage surface
 * (filesystem + memory adapters).
 *
 * - `storage status` — backend, root directory, entity/relationship counts,
 *   writer-lock holder, and operational mode (rendered from `brain.stats()`).
 * - `storage batch-delete <file>` — delete a newline-separated list of entity
 *   IDs through the public `brain.remove()` path (vectors, metadata, indexes,
 *   and statistics all stay consistent), with per-ID retry and an optional
 *   continue-on-error mode.
 *
 * The 7.x cloud-era subcommands (lifecycle policies, tiering, cost
 * estimation, runtime compression toggles) were removed with the cloud
 * storage adapters in 8.0. Filesystem gzip compression is a constructor
 * option (`storage.options.compression`, on by default), not a runtime
 * toggle.
 */

import chalk from 'chalk'
import ora from 'ora'
import inquirer from 'inquirer'
import Table from 'cli-table3'
import { readFileSync } from 'node:fs'
import { Brainy } from '../../brainy.js'

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

let brainyInstance: Brainy | null = null

/**
 * @description Lazily construct the module's shared `Brainy` instance so
 * repeated handler invocations (and tests) reuse one store handle.
 * @returns The shared `Brainy` instance.
 */
const getBrainy = (): Brainy => {
  if (!brainyInstance) {
    brainyInstance = new Brainy()
  }
  return brainyInstance
}

/**
 * @description Emit machine-readable output when `--json` is set; honors
 * `--pretty` for indented JSON.
 * @param data - The value to serialize.
 * @param options - Shared CLI flags.
 */
const formatOutput = (data: unknown, options: StorageOptions): void => {
  if (options.json) {
    console.log(options.pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data))
  }
}

/**
 * @description The `storage status` / `storage batch-delete` command handlers
 * registered in `src/cli/index.ts`.
 */
export const storageCommands = {
  /**
   * @description Show storage status: backend adapter, root directory,
   * entity/relationship counts, writer-lock holder, and operational mode.
   * @param options - Shared CLI flags.
   * @example
   * ```bash
   * brainy storage status          # human-readable report
   * brainy storage status --json   # machine-readable
   * ```
   */
  async status(options: StorageOptions) {
    const spinner = ora('Checking storage status...').start()

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

      const stats = await brain.stats()

      spinner.succeed('Storage status retrieved')

      if (options.json) {
        formatOutput(
          {
            backend: stats.storage.backend,
            rootDir: stats.storage.rootDir,
            entityCount: stats.entityCount,
            relationCount: stats.relationCount,
            mode: stats.mode,
            writerLock: stats.writerLock ?? null
          },
          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)
      }

      console.log(chalk.cyan('\n💾 Storage Status\n'))

      const infoTable = new Table({
        head: [chalk.cyan('Property'), chalk.cyan('Value')],
        style: { head: [], border: [] }
      })

      infoTable.push(
        ['Backend', chalk.green(stats.storage.backend)],
        ['Entities', String(stats.entityCount)],
        ['Relationships', String(stats.relationCount)],
        ['Mode', stats.mode]
      )
      if (stats.storage.rootDir) {
        infoTable.push(['Root', chalk.dim(stats.storage.rootDir)])
      }

      console.log(infoTable.toString())

      if (stats.writerLock) {
        console.log(chalk.bold('\nWriter Lock:'))
        console.log(
          `  PID ${stats.writerLock.pid} on ${stats.writerLock.hostname} ` +
            chalk.dim(`(since ${stats.writerLock.startedAt})`)
        )
      }

      // One-shot command — see the --json branch for why the explicit exit.
      await brain.close()
      process.exit(0)
    } catch (error: any) {
      spinner.fail('Failed to get storage status')
      console.error(chalk.red(error.message))
      process.exit(1)
    }
  },

  /**
   * @description Delete a list of entity IDs (one per line in a file) through
   * the public `brain.remove()` path so vectors, metadata, graph edges,
   * indexes, and statistics all stay consistent. Asks for confirmation before
   * deleting; failed IDs are retried up to `--max-retries` times, and
   * `--continue-on-error` keeps going past IDs that still fail.
   * @param file - Path to a newline-separated file of entity IDs.
   * @param options - Shared CLI flags plus `--max-retries` and
   *   `--continue-on-error`.
   */
  async batchDelete(
    file: string,
    options: StorageOptions & { maxRetries?: string; continueOnError?: boolean } = {}
  ) {
    const spinner = ora('Loading entity IDs...').start()

    try {
      // Read IDs from file before touching the store.
      const content = readFileSync(file, 'utf-8')
      const ids = content
        .split('\n')
        .map((line) => line.trim())
        .filter((line) => line.length > 0)

      spinner.succeed(`Loaded ${ids.length} entity IDs`)

      if (ids.length === 0) {
        console.log(chalk.yellow('No entity IDs found in file — nothing to delete'))
        process.exit(0)
      }

      // Confirm — destructive and irreversible.
      const { confirm } = await inquirer.prompt([
        {
          type: 'confirm',
          name: 'confirm',
          message: chalk.yellow(`⚠️  Delete ${ids.length} entities? This cannot be undone.`),
          default: false
        }
      ])

      if (!confirm) {
        console.log(chalk.yellow('Deletion cancelled'))
        process.exit(0)
      }

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

      const maxRetries = options.maxRetries ? parseInt(options.maxRetries, 10) : 3

      const deleteSpinner = ora(`Deleting ${ids.length} entities...`).start()
      const startTime = Date.now()
      let deleted = 0
      const failed: Array<{ id: string; error: string }> = []

      for (const id of ids) {
        let lastError: unknown = null
        let succeeded = false

        for (let attempt = 0; attempt <= maxRetries; attempt++) {
          try {
            await brain.remove(id)
            succeeded = true
            break
          } catch (error) {
            lastError = error
          }
        }

        if (succeeded) {
          deleted++
          deleteSpinner.text = `Deleting entities... ${deleted}/${ids.length}`
        } else {
          failed.push({
            id,
            error: lastError instanceof Error ? lastError.message : String(lastError)
          })
          if (!options.continueOnError) {
            break
          }
        }
      }

      const duration = ((Date.now() - startTime) / 1000).toFixed(1)

      if (failed.length === 0) {
        deleteSpinner.succeed(`Deleted ${deleted} entities in ${duration}s`)
      } else {
        deleteSpinner.warn(`Deleted ${deleted}/${ids.length} entities in ${duration}s (${failed.length} failed)`)
      }

      if (!options.json) {
        console.log(chalk.green(`\n✓ Batch delete complete`))
        console.log(chalk.dim(`  Deleted: ${deleted}`))
        console.log(chalk.dim(`  Failed: ${failed.length}`))
        console.log(chalk.dim(`  Duration: ${duration}s`))

        if (failed.length > 0 && options.verbose) {
          console.log(chalk.bold('\n⚠️  Failed IDs:'))
          for (const f of failed.slice(0, 10)) {
            console.log(`  ${chalk.dim(f.id)}: ${chalk.red(f.error)}`)
          }
          if (failed.length > 10) {
            console.log(chalk.dim(`  ... and ${failed.length - 10} more`))
          }
        }
      } else {
        formatOutput({ deleted, failed, duration: parseFloat(duration) }, options)
      }

      // One-shot command — see status() for why the explicit close + exit.
      await brain.close()
      process.exit(failed.length > 0 && !options.continueOnError ? 1 : 0)
    } catch (error: any) {
      spinner.fail('Batch delete failed')
      console.error(chalk.red(error.message))
      process.exit(1)
    }
  }
}
