/**
 * @bytelyst/field-encrypt — FieldEncryptor
 *
 * Main API — wires key provider + DEK store + cache + AES-GCM.
 * Product backends create a singleton via createFieldEncryptor().
 */

import type {
  EncryptedField,
  FieldEncryptContext,
  FieldEncryptorConfig,
  KeyProvider,
  DekStore,
} from './types.js';
import { encryptField, decryptField } from './aes-gcm.js';
import { buildDekId, getOrCreateDek, rewrapAllDeks } from './envelope.js';
import { DekCache } from './key-cache.js';
import { MemoryDekStore } from './dek-store-memory.js';
import { MemoryKeyProvider } from './key-provider-memory.js';
import { EnvKeyProvider } from './key-provider-env.js';
import { AkvKeyProvider } from './key-provider-akv.js';
import { isEncryptedField } from './guards.js';

export class FieldEncryptor {
  private readonly keyProvider: KeyProvider;
  private readonly dekStore: DekStore;
  private readonly cache: DekCache;

  constructor(config: FieldEncryptorConfig) {
    this.keyProvider = resolveKeyProvider(config);
    this.dekStore = config.dekStore ?? new MemoryDekStore();
    this.cache = new DekCache(
      config.dekCacheTtlMs ?? 15 * 60 * 1000,
      config.dekCacheMaxSize ?? 1000
    );
  }

  /**
   * Encrypt a plaintext string.
   *
   * Automatically gets or creates a DEK scoped to the userId + context.
   */
  async encrypt(plaintext: string, ctx: FieldEncryptContext): Promise<EncryptedField> {
    const dekId = buildDekId(ctx.userId, ctx.context);
    const aad = `${ctx.userId}:${ctx.context}`;
    const dek = await getOrCreateDek(dekId, this.keyProvider, this.dekStore, this.cache);
    return encryptField(plaintext, dek, dekId, aad);
  }

  /**
   * Decrypt an EncryptedField back to plaintext.
   */
  async decrypt(field: EncryptedField, ctx: FieldEncryptContext): Promise<string> {
    const aad = `${ctx.userId}:${ctx.context}`;
    const dek = await getOrCreateDek(field.dekId, this.keyProvider, this.dekStore, this.cache);
    return decryptField(field, dek, aad);
  }

  /**
   * Encrypt multiple fields in a single call (optimized — single DEK lookup).
   */
  async encryptBatch(plaintexts: string[], ctx: FieldEncryptContext): Promise<EncryptedField[]> {
    const dekId = buildDekId(ctx.userId, ctx.context);
    const aad = `${ctx.userId}:${ctx.context}`;
    const dek = await getOrCreateDek(dekId, this.keyProvider, this.dekStore, this.cache);
    return plaintexts.map(pt => encryptField(pt, dek, dekId, aad));
  }

  /**
   * Decrypt multiple EncryptedFields in a single call.
   *
   * Groups by dekId for efficient DEK lookup.
   */
  async decryptBatch(fields: EncryptedField[], ctx: FieldEncryptContext): Promise<string[]> {
    const aad = `${ctx.userId}:${ctx.context}`;
    const dekMap = new Map<string, Buffer>();

    const results: string[] = [];
    for (const field of fields) {
      let dek = dekMap.get(field.dekId);
      if (!dek) {
        dek = await getOrCreateDek(field.dekId, this.keyProvider, this.dekStore, this.cache);
        dekMap.set(field.dekId, dek);
      }
      results.push(decryptField(field, dek, aad));
    }

    return results;
  }

  /**
   * Check if a value is an EncryptedField.
   */
  isEncrypted(value: unknown): value is EncryptedField {
    return isEncryptedField(value);
  }

  /**
   * Re-wrap all DEKs after MEK rotation.
   */
  async rewrapDeks(
    newKeyProvider: KeyProvider,
    onProgress?: (completed: number, total: number) => void
  ): Promise<number> {
    return rewrapAllDeks(this.keyProvider, newKeyProvider, this.dekStore, this.cache, onProgress);
  }

  /** DEK cache hit rate (0-100). */
  get cacheHitRate(): number {
    return this.cache.hitRate;
  }

  /** Number of cached DEKs. */
  get cacheSize(): number {
    return this.cache.size;
  }

  /** Reset cache statistics. */
  resetCacheStats(): void {
    this.cache.resetStats();
  }

  /** Clear DEK cache (e.g., on shutdown). */
  clearCache(): void {
    this.cache.clear();
  }
}

function resolveKeyProvider(config: FieldEncryptorConfig): KeyProvider {
  switch (config.keyProvider) {
    case 'memory':
      return new MemoryKeyProvider();

    case 'env': {
      const key = config.encryptionKey;
      if (!key) {
        throw new Error('FieldEncryptor: "env" key provider requires encryptionKey (hex string)');
      }
      return new EnvKeyProvider(key);
    }

    case 'akv': {
      if (!config.keyVaultUrl) {
        throw new Error('FieldEncryptor: "akv" key provider requires keyVaultUrl');
      }
      if (!config.mekName) {
        throw new Error('FieldEncryptor: "akv" key provider requires mekName');
      }
      return new AkvKeyProvider(config.keyVaultUrl, config.mekName);
    }

    default:
      throw new Error(`FieldEncryptor: unknown key provider "${config.keyProvider}"`);
  }
}

/**
 * No-op encryptor — stores/returns plaintext unchanged.
 *
 * Returned by createFieldEncryptor({ enabled: false, ... }).
 * All repositories continue calling encrypt()/decrypt() without branching.
 */
export class NullFieldEncryptor extends FieldEncryptor {
  constructor() {
    // Use memory provider — it will never be called, but satisfies the constructor
    super({ keyProvider: 'memory' });
  }

  override async encrypt(plaintext: string, _ctx: FieldEncryptContext): Promise<EncryptedField> {
    // Return a sentinel object that looks encrypted but stores plaintext
    return {
      __encrypted: true,
      v: 1,
      alg: 'aes-256-gcm',
      ct: plaintext,
      iv: 'disabled',
      tag: 'disabled',
      dekId: 'disabled',
    };
  }

  override async decrypt(field: EncryptedField, _ctx: FieldEncryptContext): Promise<string> {
    // If encryption was disabled, ct contains the plaintext directly
    return field.ct;
  }

  override async encryptBatch(
    plaintexts: string[],
    ctx: FieldEncryptContext
  ): Promise<EncryptedField[]> {
    return Promise.all(plaintexts.map(pt => this.encrypt(pt, ctx)));
  }

  override async decryptBatch(
    fields: EncryptedField[],
    ctx: FieldEncryptContext
  ): Promise<string[]> {
    return Promise.all(fields.map(f => this.decrypt(f, ctx)));
  }
}

/**
 * Create a FieldEncryptor instance.
 *
 * Typical usage (one per backend service):
 * ```typescript
 * const encryptor = createFieldEncryptor({
 *   keyProvider: config.FIELD_ENCRYPT_KEY_PROVIDER ?? 'memory',
 *   mekName: 'lysnr-mek',
 *   keyVaultUrl: config.AZURE_KEYVAULT_URL,
 * });
 * ```
 *
 * To disable encryption globally or per-product:
 * ```typescript
 * const encryptor = createFieldEncryptor({
 *   enabled: false,     // ← no-op passthrough
 *   keyProvider: 'memory',
 * });
 * ```
 */
export function createFieldEncryptor(config: FieldEncryptorConfig): FieldEncryptor {
  if (config.enabled === false) {
    return new NullFieldEncryptor();
  }
  return new FieldEncryptor(config);
}
