/**
 * @bytelyst/field-encrypt — Migration helpers
 *
 * Utilities for encrypting existing plaintext fields in-place.
 * Idempotent — skips already-encrypted fields via __encrypted sentinel.
 */

import type { EncryptedField } from './types.js';
import { isEncryptedField } from './guards.js';

/** Result of a migration run. */
export interface MigrationResult {
  /** Total documents scanned. */
  scanned: number;
  /** Documents encrypted in this run. */
  encrypted: number;
  /** Documents skipped (already encrypted). */
  skipped: number;
  /** Documents that failed to encrypt. */
  errors: number;
  /** Error details (first 10). */
  errorDetails: Array<{ id: string; error: string }>;
}

/** Options for migrateDocuments(). */
export interface MigrateDocumentsOptions<T> {
  /** Fetch a batch of documents. Return empty array when done. */
  fetchBatch: (offset: number, batchSize: number) => Promise<T[]>;
  /** Get the document ID for logging. */
  getId: (doc: T) => string;
  /** Get the field value to check/encrypt. */
  getField: (doc: T) => unknown;
  /** Encrypt the plaintext value. Returns the EncryptedField. */
  encryptValue: (plaintext: string, doc: T) => Promise<EncryptedField>;
  /** Write the encrypted value back to the store. */
  writeBack: (doc: T, encrypted: EncryptedField) => Promise<void>;
  /** Batch size (default: 100). */
  batchSize?: number;
  /** If true, don't write — just count. */
  dryRun?: boolean;
  /** Progress callback. */
  onProgress?: (result: MigrationResult) => void;
}

/**
 * Migrate plaintext fields to encrypted fields in batches.
 *
 * Idempotent: skips documents where the field is already an EncryptedField.
 */
export async function migrateDocuments<T>(
  options: MigrateDocumentsOptions<T>
): Promise<MigrationResult> {
  const batchSize = options.batchSize ?? 100;
  const result: MigrationResult = {
    scanned: 0,
    encrypted: 0,
    skipped: 0,
    errors: 0,
    errorDetails: [],
  };

  let offset = 0;
  let batch: T[];

  do {
    batch = await options.fetchBatch(offset, batchSize);

    for (const doc of batch) {
      result.scanned++;
      const fieldValue = options.getField(doc);

      // Skip already-encrypted
      if (isEncryptedField(fieldValue)) {
        result.skipped++;
        continue;
      }

      // Skip null/undefined
      if (fieldValue == null) {
        result.skipped++;
        continue;
      }

      const plaintext = typeof fieldValue === 'string' ? fieldValue : JSON.stringify(fieldValue);

      try {
        const encrypted = await options.encryptValue(plaintext, doc);

        if (!options.dryRun) {
          await options.writeBack(doc, encrypted);
        }

        result.encrypted++;
      } catch (err) {
        result.errors++;
        if (result.errorDetails.length < 10) {
          result.errorDetails.push({
            id: options.getId(doc),
            error: err instanceof Error ? err.message : String(err),
          });
        }
      }
    }

    offset += batch.length;
    options.onProgress?.(result);
  } while (batch.length === batchSize);

  return result;
}
