#!/usr/bin/env npx tsx
/* eslint-disable no-console */
/**
 * Encryption Migration CLI — Batch-encrypt existing plaintext Cosmos documents.
 *
 * Uses @bytelyst/field-encrypt migrateDocuments() to idempotently encrypt
 * plaintext fields across all product backends.
 *
 * Usage:
 *   npx tsx scripts/encrypt-migrate.ts --product lysnrai --dry-run
 *   npx tsx scripts/encrypt-migrate.ts --product lysnrai --container transcripts
 *   npx tsx scripts/encrypt-migrate.ts --product all --dry-run
 *   npx tsx scripts/encrypt-migrate.ts --product chronomind --batch-size 50
 *
 * Required env vars:
 *   COSMOS_ENDPOINT     — Azure Cosmos DB endpoint
 *   COSMOS_KEY          — Azure Cosmos DB key
 *   COSMOS_DATABASE     — Database name (default: lysnrai)
 *   FIELD_ENCRYPT_KEY_PROVIDER — 'memory' | 'env' | 'akv' (default: memory)
 *   FIELD_ENCRYPT_KEY   — Hex key (for 'env' provider)
 *   AZURE_KEYVAULT_URL  — AKV URL (for 'akv' provider)
 *
 * Per-product MEK names are defined in migration configs.
 */

import { CosmosClient, type Container } from '@azure/cosmos';
import {
  createFieldEncryptor,
  migrateDocuments,
  type MigrationResult,
  type FieldEncryptor,
  type EncryptedField,
} from '@bytelyst/field-encrypt';

// ── Types ──────────────────────────────────────────────

interface FieldSpec {
  /** The field path (dot-separated for nested, e.g. 'triageResult.summary'). */
  field: string;
  /** Encryption context label. */
  context: string;
  /** The property on the doc that holds the userId for DEK scoping. Default: 'userId'. */
  userIdField?: string;
  /** If true, the field is a string[] or Record — JSON-serialize before encrypting. */
  jsonSerialize?: boolean;
}

interface ContainerMigrationConfig {
  /** Cosmos container name. */
  container: string;
  /** Partition key path (e.g. '/userId'). */
  partitionKey: string;
  /** Fields to encrypt in this container. */
  fields: FieldSpec[];
}

interface ProductMigrationConfig {
  /** Product ID. */
  productId: string;
  /** MEK name for this product. */
  mekName: string;
  /** Containers to migrate. */
  containers: ContainerMigrationConfig[];
}

// ── Field accessor helpers ────────────────────────────

function getNestedField(doc: Record<string, unknown>, path: string): unknown {
  const parts = path.split('.');
  let current: unknown = doc;
  for (const part of parts) {
    if (current == null || typeof current !== 'object') return undefined;
    current = (current as Record<string, unknown>)[part];
  }
  return current;
}

function setNestedField(
  doc: Record<string, unknown>,
  path: string,
  value: unknown
): Record<string, unknown> {
  const parts = path.split('.');
  const result = { ...doc };

  if (parts.length === 1) {
    result[parts[0]] = value;
    return result;
  }

  // Deep clone the nested path
  let current = result;
  for (let i = 0; i < parts.length - 1; i++) {
    const nested = current[parts[i]];
    if (nested == null || typeof nested !== 'object') return result;
    current[parts[i]] = { ...(nested as Record<string, unknown>) };
    current = current[parts[i]] as Record<string, unknown>;
  }
  current[parts[parts.length - 1]] = value;
  return result;
}

// ── Product migration configs ─────────────────────────

const PRODUCT_CONFIGS: ProductMigrationConfig[] = [
  // ── LysnrAI (port 4015) ──
  {
    productId: 'lysnrai',
    mekName: 'lysnr-mek',
    containers: [
      {
        container: 'transcripts',
        partitionKey: '/userId',
        fields: [{ field: 'transcriptText', context: 'transcripts' }],
      },
      {
        container: 'sessions',
        partitionKey: '/userId',
        fields: [{ field: 'notes', context: 'sessions' }],
      },
    ],
  },
  // ── JarvisJr (port 4012) ──
  {
    productId: 'jarvisjr',
    mekName: 'jarvisjr-mek',
    containers: [
      {
        container: 'jarvis_sessions',
        partitionKey: '/userId',
        fields: [{ field: 'notes', context: 'jarvis-sessions' }],
      },
      {
        container: 'jarvis_memory',
        partitionKey: '/agentId',
        fields: [{ field: 'content', context: 'jarvis-memory', userIdField: 'agentId' }],
      },
    ],
  },
  // ── NoteLett (port 4016) ──
  {
    productId: 'notelett',
    mekName: 'notelett-mek',
    containers: [
      {
        container: 'notes',
        partitionKey: '/workspaceId',
        fields: [{ field: 'body', context: 'notes', userIdField: 'createdBy' }],
      },
    ],
  },
  // ── NomGap (port 4013) ──
  {
    productId: 'nomgap',
    mekName: 'nomgap-mek',
    containers: [
      {
        container: 'meal_logs',
        partitionKey: '/userId',
        fields: [{ field: 'notes', context: 'meal-logs' }],
      },
      {
        container: 'fasting_sessions',
        partitionKey: '/userId',
        fields: [{ field: 'notes', context: 'fasting-sessions' }],
      },
      {
        container: 'weight_log',
        partitionKey: '/userId',
        fields: [{ field: 'notes', context: 'weight-log' }],
      },
    ],
  },
  // ── ActionTrail (port 4018) ──
  {
    productId: 'actiontrail',
    mekName: 'actiontrail-mek',
    containers: [
      {
        container: 'trail_actions',
        partitionKey: '/userId',
        fields: [
          { field: 'before', context: 'trail-actions', jsonSerialize: true },
          { field: 'after', context: 'trail-actions-after', jsonSerialize: true },
        ],
      },
      {
        container: 'trail_reverts',
        partitionKey: '/userId',
        fields: [
          { field: 'beforeState', context: 'trail-reverts', jsonSerialize: true },
          { field: 'afterState', context: 'trail-reverts-after', jsonSerialize: true },
          { field: 'revertPayload', context: 'trail-reverts-payload', jsonSerialize: true },
        ],
      },
    ],
  },
  // ── FlowMonk (port 4017) ──
  {
    productId: 'flowmonk',
    mekName: 'flowmonk-mek',
    containers: [
      {
        container: 'tasks',
        partitionKey: '/userId',
        fields: [{ field: 'description', context: 'tasks' }],
      },
    ],
  },
  // ── ChronoMind (port 4011) ──
  {
    productId: 'chronomind',
    mekName: 'chronomind-mek',
    containers: [
      {
        container: 'timers',
        partitionKey: '/userId',
        fields: [{ field: 'description', context: 'timers' }],
      },
      {
        container: 'routines',
        partitionKey: '/userId',
        fields: [
          { field: 'description', context: 'routines' },
          // steps[].notes handled differently — see note below
        ],
      },
      {
        container: 'shared_timers',
        partitionKey: '/householdId',
        fields: [{ field: 'description', context: 'shared-timers', userIdField: 'createdBy' }],
      },
      {
        container: 'webhook_subscriptions',
        partitionKey: '/userId',
        fields: [
          { field: 'secret', context: 'webhooks' },
          { field: 'description', context: 'webhook-desc' },
        ],
      },
    ],
  },
  // ── PeakPulse (port 4010) ──
  {
    productId: 'peakpulse',
    mekName: 'peakpulse-mek',
    containers: [
      {
        container: 'peak_sessions',
        partitionKey: '/userId',
        fields: [{ field: 'notes', context: 'peak-sessions' }],
      },
    ],
  },
  // ── MindLyst (port 4014) ──
  {
    productId: 'mindlyst',
    mekName: 'mindlyst-mek',
    containers: [
      {
        container: 'memory_items',
        partitionKey: '/userId',
        fields: [
          { field: 'rawContent', context: 'memory' },
          { field: 'triageResult.summary', context: 'memory-triage' },
        ],
      },
      {
        container: 'reflections',
        partitionKey: '/userId',
        fields: [
          { field: 'repeatedThemes', context: 'reflection-themes', jsonSerialize: true },
          { field: 'postponedItems', context: 'reflection-postponed', jsonSerialize: true },
          { field: 'roleImbalanceSignals', context: 'reflection-imbalance', jsonSerialize: true },
          { field: 'suggestedAdjustments', context: 'reflection-adjustments', jsonSerialize: true },
          { field: 'vsLastWeek.summary', context: 'reflection-vs' },
        ],
      },
      {
        container: 'daily_briefs',
        partitionKey: '/userId',
        fields: [
          { field: 'greeting', context: 'daily-briefs' },
          { field: 'priorityItems', context: 'brief-priorities', jsonSerialize: true },
          { field: 'brainSummaries', context: 'brief-summaries', jsonSerialize: true },
          { field: 'streakMessage', context: 'brief-streak' },
          { field: 'motivationalQuote', context: 'brief-quote' },
        ],
      },
      {
        container: 'brains',
        partitionKey: '/userId',
        fields: [{ field: 'rolePrompt', context: 'brains' }],
      },
    ],
  },
];

// ── CLI argument parsing ──────────────────────────────

interface CliArgs {
  product: string;
  container?: string;
  batchSize: number;
  dryRun: boolean;
  verbose: boolean;
}

function parseArgs(): CliArgs {
  const args = process.argv.slice(2);
  const result: CliArgs = {
    product: '',
    batchSize: 100,
    dryRun: false,
    verbose: false,
  };

  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case '--product':
      case '-p':
        result.product = args[++i] ?? '';
        break;
      case '--container':
      case '-c':
        result.container = args[++i];
        break;
      case '--batch-size':
      case '-b':
        result.batchSize = parseInt(args[++i] ?? '100', 10);
        break;
      case '--dry-run':
      case '-n':
        result.dryRun = true;
        break;
      case '--verbose':
      case '-v':
        result.verbose = true;
        break;
      case '--help':
      case '-h':
        printUsage();
        process.exit(0);
        break;
      default:
        console.error(`Unknown argument: ${args[i]}`);
        printUsage();
        process.exit(1);
    }
  }

  if (!result.product) {
    console.error('Error: --product is required\n');
    printUsage();
    process.exit(1);
  }

  return result;
}

function printUsage(): void {
  console.log(`
╔══════════════════════════════════════════════════════════════╗
║  ByteLyst Encryption Migration CLI                         ║
║  Batch-encrypt existing plaintext Cosmos documents         ║
╚══════════════════════════════════════════════════════════════╝

Usage:
  npx tsx scripts/encrypt-migrate.ts --product <id> [options]

Options:
  --product, -p <id>      Product ID or 'all' (required)
  --container, -c <name>  Migrate only this container (optional)
  --batch-size, -b <n>    Documents per batch (default: 100)
  --dry-run, -n           Count without encrypting
  --verbose, -v           Show per-document progress
  --help, -h              Show this help

Products: ${PRODUCT_CONFIGS.map(c => c.productId).join(', ')}

Environment:
  COSMOS_ENDPOINT              Azure Cosmos DB endpoint
  COSMOS_KEY                   Azure Cosmos DB key
  COSMOS_DATABASE              Database name (default: lysnrai)
  FIELD_ENCRYPT_KEY_PROVIDER   'memory' | 'env' | 'akv' (default: memory)
  FIELD_ENCRYPT_KEY            Hex key (for 'env' provider)
  AZURE_KEYVAULT_URL           AKV URL (for 'akv' provider)

Examples:
  npx tsx scripts/encrypt-migrate.ts --product lysnrai --dry-run
  npx tsx scripts/encrypt-migrate.ts --product all --dry-run
  npx tsx scripts/encrypt-migrate.ts --product chronomind --container timers
  npx tsx scripts/encrypt-migrate.ts --product mindlyst --batch-size 50
`);
}

// ── Cosmos helpers ────────────────────────────────────

function createCosmosClient(): CosmosClient {
  const endpoint = process.env.COSMOS_ENDPOINT;
  const key = process.env.COSMOS_KEY;

  if (!endpoint || !key) {
    console.error('Error: COSMOS_ENDPOINT and COSMOS_KEY environment variables are required');
    process.exit(1);
  }

  return new CosmosClient({ endpoint, key });
}

function getPartitionKeyValue(doc: Record<string, unknown>, partitionKeyPath: string): string {
  const field = partitionKeyPath.replace(/^\//, '');
  return String(doc[field] ?? '');
}

// ── Migration engine ──────────────────────────────────

interface MigrateFieldResult {
  containerName: string;
  field: string;
  result: MigrationResult;
}

async function migrateContainerField(
  cosmosContainer: Container,
  partitionKey: string,
  fieldSpec: FieldSpec,
  encryptor: FieldEncryptor,
  batchSize: number,
  dryRun: boolean,
  verbose: boolean
): Promise<MigrateFieldResult> {
  const containerName = cosmosContainer.id;
  const userIdProp = fieldSpec.userIdField ?? 'userId';

  const result = await migrateDocuments<Record<string, unknown>>({
    batchSize,
    dryRun,

    fetchBatch: async (offset: number, size: number) => {
      const query = `SELECT * FROM c ORDER BY c._ts OFFSET ${offset} LIMIT ${size}`;
      const { resources } = await cosmosContainer.items.query(query).fetchAll();
      return resources;
    },

    getId: (doc: Record<string, unknown>) => String(doc.id ?? 'unknown'),

    getField: (doc: Record<string, unknown>) => getNestedField(doc, fieldSpec.field),

    encryptValue: async (plaintext: string, doc: Record<string, unknown>) => {
      const userId = String(doc[userIdProp] ?? 'unknown');
      return encryptor.encrypt(plaintext, {
        userId,
        context: fieldSpec.context,
      });
    },

    writeBack: async (doc: Record<string, unknown>, encrypted: EncryptedField) => {
      const updated = setNestedField(doc, fieldSpec.field, encrypted);
      const pk = getPartitionKeyValue(doc, partitionKey);
      await cosmosContainer.item(String(doc.id), pk).replace(updated);
    },

    onProgress: (progress: MigrationResult) => {
      if (verbose) {
        const mode = dryRun ? '[DRY-RUN]' : '[LIVE]';
        process.stdout.write(
          `\r  ${mode} ${containerName}.${fieldSpec.field}: scanned=${progress.scanned} encrypted=${progress.encrypted} skipped=${progress.skipped} errors=${progress.errors}`
        );
      }
    },
  });

  if (verbose) {
    process.stdout.write('\n');
  }

  return { containerName, field: fieldSpec.field, result };
}

async function migrateProduct(
  config: ProductMigrationConfig,
  cosmosClient: CosmosClient,
  args: CliArgs
): Promise<MigrateFieldResult[]> {
  const dbName = process.env.COSMOS_DATABASE || 'lysnrai';
  const database = cosmosClient.database(dbName);

  const encryptor = createFieldEncryptor({
    keyProvider: (process.env.FIELD_ENCRYPT_KEY_PROVIDER as 'memory' | 'env' | 'akv') ?? 'memory',
    encryptionKey: process.env.FIELD_ENCRYPT_KEY || undefined,
    keyVaultUrl: process.env.AZURE_KEYVAULT_URL || undefined,
    mekName: config.mekName,
  });

  const results: MigrateFieldResult[] = [];

  for (const containerConfig of config.containers) {
    // Skip if user specified a specific container
    if (args.container && containerConfig.container !== args.container) {
      continue;
    }

    const cosmosContainer = database.container(containerConfig.container);
    const mode = args.dryRun ? 'DRY-RUN' : 'LIVE';

    console.log(`\n  📦 ${containerConfig.container} (${mode})`);

    for (const fieldSpec of containerConfig.fields) {
      console.log(`    🔐 Field: ${fieldSpec.field} (context: ${fieldSpec.context})`);

      try {
        const fieldResult = await migrateContainerField(
          cosmosContainer,
          containerConfig.partitionKey,
          fieldSpec,
          encryptor,
          args.batchSize,
          args.dryRun,
          args.verbose
        );

        results.push(fieldResult);

        const r = fieldResult.result;
        console.log(
          `       scanned=${r.scanned} encrypted=${r.encrypted} skipped=${r.skipped} errors=${r.errors}`
        );

        if (r.errorDetails.length > 0) {
          console.log('       Errors:');
          for (const err of r.errorDetails) {
            console.log(`         - ${err.id}: ${err.error}`);
          }
        }
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        console.error(`       ERROR: ${message}`);
        results.push({
          containerName: containerConfig.container,
          field: fieldSpec.field,
          result: {
            scanned: 0,
            encrypted: 0,
            skipped: 0,
            errors: 1,
            errorDetails: [{ id: 'container', error: message }],
          },
        });
      }
    }
  }

  return results;
}

// ── Main ──────────────────────────────────────────────

async function main(): Promise<void> {
  const args = parseArgs();
  const startTime = Date.now();

  console.log('\n════════════════════════════════════════════════════');
  console.log('  ByteLyst Encryption Migration CLI');
  console.log('════════════════════════════════════════════════════');
  console.log(`  Mode:       ${args.dryRun ? 'DRY-RUN (no writes)' : 'LIVE (encrypting!)'}`);
  console.log(`  Product:    ${args.product}`);
  console.log(`  Container:  ${args.container ?? 'all'}`);
  console.log(`  Batch size: ${args.batchSize}`);
  console.log(`  Database:   ${process.env.COSMOS_DATABASE || 'lysnrai'}`);
  console.log(`  Key provider: ${process.env.FIELD_ENCRYPT_KEY_PROVIDER || 'memory'}`);
  console.log('════════════════════════════════════════════════════');

  const cosmosClient = createCosmosClient();
  const allResults: MigrateFieldResult[] = [];

  const productsToMigrate =
    args.product === 'all'
      ? PRODUCT_CONFIGS
      : PRODUCT_CONFIGS.filter(c => c.productId === args.product);

  if (productsToMigrate.length === 0) {
    console.error(`\nError: Unknown product '${args.product}'`);
    console.error(`Available: ${PRODUCT_CONFIGS.map(c => c.productId).join(', ')}`);
    process.exit(1);
  }

  for (const productConfig of productsToMigrate) {
    console.log(`\n🏢 ${productConfig.productId} (MEK: ${productConfig.mekName})`);
    const results = await migrateProduct(productConfig, cosmosClient, args);
    allResults.push(...results);
  }

  // ── Summary ──
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
  const totals = allResults.reduce(
    (acc, r) => ({
      scanned: acc.scanned + r.result.scanned,
      encrypted: acc.encrypted + r.result.encrypted,
      skipped: acc.skipped + r.result.skipped,
      errors: acc.errors + r.result.errors,
    }),
    { scanned: 0, encrypted: 0, skipped: 0, errors: 0 }
  );

  console.log('\n════════════════════════════════════════════════════');
  console.log('  Migration Summary');
  console.log('════════════════════════════════════════════════════');
  console.log(`  Products:   ${productsToMigrate.length}`);
  console.log(`  Fields:     ${allResults.length}`);
  console.log(`  Scanned:    ${totals.scanned}`);
  console.log(`  Encrypted:  ${totals.encrypted}`);
  console.log(`  Skipped:    ${totals.skipped}`);
  console.log(`  Errors:     ${totals.errors}`);
  console.log(`  Duration:   ${elapsed}s`);
  console.log(`  Mode:       ${args.dryRun ? 'DRY-RUN' : 'LIVE'}`);
  console.log('════════════════════════════════════════════════════\n');

  // Per-field breakdown
  if (allResults.length > 0) {
    console.log('  Per-field breakdown:');
    console.log('  ┌─────────────────────────────┬────────┬───────────┬─────────┬────────┐');
    console.log('  │ Container.Field              │ Scanned│ Encrypted │ Skipped │ Errors │');
    console.log('  ├─────────────────────────────┼────────┼───────────┼─────────┼────────┤');
    for (const r of allResults) {
      const name = `${r.containerName}.${r.field}`.padEnd(29);
      const sc = String(r.result.scanned).padStart(7);
      const en = String(r.result.encrypted).padStart(9);
      const sk = String(r.result.skipped).padStart(7);
      const er = String(r.result.errors).padStart(6);
      console.log(`  │ ${name}│${sc} │${en} │${sk} │${er} │`);
    }
    console.log('  └─────────────────────────────┴────────┴───────────┴─────────┴────────┘');
  }

  if (totals.errors > 0) {
    process.exit(1);
  }
}

main().catch(err => {
  console.error('\nFatal error:', err instanceof Error ? err.message : err);
  process.exit(1);
});
