/**
 * AES-256-GCM encryption for credential storage
 */

import * as plugins from '../plugins.js';
import { logger } from '../logging.js';

export class CredentialEncryption {
  private key: CryptoKey | null = null;
  private readonly algorithm = 'AES-GCM';
  private readonly keyLength = 256;
  private readonly ivLength = 12; // 96 bits for GCM

  /**
   * Initialize encryption with a key from environment or generate machine-specific key
   */
  async init(): Promise<void> {
    const envKey = process.env.ONEBOX_ENCRYPTION_KEY;

    if (envKey) {
      // Use provided key (should be 32 bytes base64 encoded)
      const keyBytes = this.base64ToBytes(envKey);
      if (keyBytes.length !== 32) {
        throw new Error('ONEBOX_ENCRYPTION_KEY must be 32 bytes (256 bits) base64 encoded');
      }
      this.key = await crypto.subtle.importKey(
        'raw',
        keyBytes.buffer as ArrayBuffer,
        { name: this.algorithm },
        false,
        ['encrypt', 'decrypt']
      );
      logger.log('info', 'Encryption key loaded from environment', 'CredentialEncryption');
    } else {
      // Derive key from machine-specific data
      this.key = await this.deriveKeyFromMachine();
      logger.log('info', 'Encryption key derived from machine identity', 'CredentialEncryption');
    }
  }

  /**
   * Derive a key from machine-specific information
   * This ensures the key is consistent across restarts on the same machine
   */
  private async deriveKeyFromMachine(): Promise<CryptoKey> {
    // Collect machine-specific data
    const machineData: string[] = [];

    // Hostname
    try {
      machineData.push(plugins.nodeOs.hostname());
    } catch {
      machineData.push('unknown-host');
    }

    // Machine ID from /etc/machine-id (Linux) or generate consistent fallback
    try {
      const machineId = await plugins.fs.readTextFile('/etc/machine-id');
      machineData.push(machineId.trim());
    } catch {
      // Fallback: use a combination of other identifiers
      machineData.push('onebox-default-machine-id');
    }

    // Add a salt
    machineData.push('onebox-credential-encryption-v1');

    // Create seed from machine data
    const seed = machineData.join(':');
    const encoder = new TextEncoder();
    const seedBytes = encoder.encode(seed);

    // Use PBKDF2 to derive key
    const baseKey = await crypto.subtle.importKey(
      'raw',
      seedBytes,
      'PBKDF2',
      false,
      ['deriveKey']
    );

    return await crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: encoder.encode('onebox-salt-v1'),
        iterations: 100000,
        hash: 'SHA-256',
      },
      baseKey,
      { name: this.algorithm, length: this.keyLength },
      false,
      ['encrypt', 'decrypt']
    );
  }

  /**
   * Encrypt a credentials object to a base64 string
   */
  async encrypt(data: Record<string, string>): Promise<string> {
    if (!this.key) {
      await this.init();
    }
    const key = this.key;
    if (!key) {
      throw new Error('Encryption key initialization failed.');
    }

    const iv = crypto.getRandomValues(new Uint8Array(this.ivLength));
    const encoded = new TextEncoder().encode(JSON.stringify(data));

    const ciphertext = await crypto.subtle.encrypt(
      { name: this.algorithm, iv },
      key,
      encoded
    );

    // Combine IV + ciphertext and encode as base64
    const combined = new Uint8Array(iv.length + ciphertext.byteLength);
    combined.set(iv, 0);
    combined.set(new Uint8Array(ciphertext), iv.length);

    return this.bytesToBase64(combined);
  }

  /**
   * Decrypt a base64 string back to credentials object
   */
  async decrypt<T extends Record<string, string> = Record<string, string>>(
    encrypted: string,
  ): Promise<T> {
    if (!this.key) {
      await this.init();
    }
    const key = this.key;
    if (!key) {
      throw new Error('Encryption key initialization failed.');
    }

    const combined = this.base64ToBytes(encrypted);

    // Extract IV and ciphertext
    const iv = combined.slice(0, this.ivLength);
    const ciphertext = combined.slice(this.ivLength);

    const decrypted = await crypto.subtle.decrypt(
      { name: this.algorithm, iv },
      key,
      ciphertext
    );

    const decoded = new TextDecoder().decode(decrypted);
    return JSON.parse(decoded) as T;
  }

  /**
   * Generate a secure random password
   */
  generatePassword(length: number = 32): string {
    // Exclude ambiguous characters (0, O, l, 1, I)
    const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
    const randomBytes = crypto.getRandomValues(new Uint8Array(length));
    let result = '';
    for (const byte of randomBytes) {
      result += chars[byte % chars.length];
    }
    return result;
  }

  /**
   * Generate an access key (alphanumeric, uppercase)
   */
  generateAccessKey(length: number = 20): string {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    const randomBytes = crypto.getRandomValues(new Uint8Array(length));
    let result = '';
    for (const byte of randomBytes) {
      result += chars[byte % chars.length];
    }
    return result;
  }

  /**
   * Generate a secret key (alphanumeric, mixed case)
   */
  generateSecretKey(length: number = 40): string {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const randomBytes = crypto.getRandomValues(new Uint8Array(length));
    let result = '';
    for (const byte of randomBytes) {
      result += chars[byte % chars.length];
    }
    return result;
  }

  private bytesToBase64(bytes: Uint8Array): string {
    let binary = '';
    for (const byte of bytes) {
      binary += String.fromCharCode(byte);
    }
    return btoa(binary);
  }

  private base64ToBytes(base64: string): Uint8Array {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes;
  }
}

// Singleton instance
export const credentialEncryption = new CredentialEncryption();
