import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';

/**
 * Service for encrypting and decrypting sensitive data using AES-256-GCM
 *
 * Security features:
 * - AES-256-GCM encryption with authenticated encryption
 * - Random IV for each encryption operation
 * - Auth tag for data integrity verification
 * - Secure key derivation from environment variable
 */
@Injectable()
export class EncryptionService implements OnModuleInit {
  private readonly logger = new Logger(EncryptionService.name);
  private readonly algorithm = 'aes-256-gcm';
  private readonly ivLength = 16; // 128 bits
  private readonly tagLength = 16; // 128 bits
  private readonly keyLength = 32; // 256 bits
  private encryptionKey: Buffer;

  constructor(private readonly configService: ConfigService) {}

  onModuleInit(): void {
    const key = this.configService.get<string>('ENCRYPTION_KEY');
    const nodeEnv = this.configService.get<string>('NODE_ENV', 'development');
    const isProduction = nodeEnv === 'production';

    if (!key) {
      if (isProduction) {
        // CRITICAL: In production, ENCRYPTION_KEY must be set
        throw new Error(
          'ENCRYPTION_KEY environment variable is required in production. ' +
          'Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'
        );
      }

      this.logger.warn(
        'ENCRYPTION_KEY not configured. Using derived key from JWT_SECRET. ' +
        'This is acceptable for development but NOT for production!',
      );
      // Fallback for development only: derive key from JWT_SECRET
      const jwtSecret = this.configService.get<string>('JWT_SECRET', 'dev-fallback-key');
      this.encryptionKey = this.deriveKey(jwtSecret);
    } else if (key.length < this.keyLength) {
      // Derive a proper key from the provided string
      this.encryptionKey = this.deriveKey(key);
    } else {
      // Use the first 32 bytes if key is longer
      this.encryptionKey = Buffer.from(key.slice(0, this.keyLength), 'utf-8');
    }
  }

  /**
   * Derives a 256-bit key from a password/passphrase using PBKDF2
   */
  private deriveKey(password: string): Buffer {
    // Using a fixed salt is acceptable here since we're just deriving
    // an encryption key from a secret, not storing passwords
    const salt = 'tos-encryption-salt-v1';
    return crypto.pbkdf2Sync(password, salt, 100000, this.keyLength, 'sha256');
  }

  /**
   * Encrypts a string using AES-256-GCM
   *
   * @param plaintext - The string to encrypt
   * @returns Base64-encoded string containing IV + ciphertext + auth tag
   */
  encrypt(plaintext: string): string {
    if (!plaintext) {
      return '';
    }

    // Generate random IV for each encryption
    const iv = crypto.randomBytes(this.ivLength);

    // Create cipher
    const cipher = crypto.createCipheriv(this.algorithm, this.encryptionKey, iv, {
      authTagLength: this.tagLength,
    });

    // Encrypt the data
    const encrypted = Buffer.concat([
      cipher.update(plaintext, 'utf8'),
      cipher.final(),
    ]);

    // Get the auth tag
    const authTag = cipher.getAuthTag();

    // Combine IV + ciphertext + authTag and encode as base64
    const combined = Buffer.concat([iv, encrypted, authTag]);

    return combined.toString('base64');
  }

  /**
   * Decrypts a string that was encrypted with the encrypt method
   *
   * @param encryptedData - Base64-encoded string from encrypt()
   * @returns The original plaintext string
   * @throws Error if decryption fails (tampered data or wrong key)
   */
  decrypt(encryptedData: string): string {
    if (!encryptedData) {
      return '';
    }

    try {
      // Decode the combined data
      const combined = Buffer.from(encryptedData, 'base64');

      // Extract IV, ciphertext, and auth tag
      const iv = combined.subarray(0, this.ivLength);
      const authTag = combined.subarray(combined.length - this.tagLength);
      const ciphertext = combined.subarray(this.ivLength, combined.length - this.tagLength);

      // Create decipher
      const decipher = crypto.createDecipheriv(this.algorithm, this.encryptionKey, iv, {
        authTagLength: this.tagLength,
      });

      // Set the auth tag for verification
      decipher.setAuthTag(authTag);

      // Decrypt the data
      const decrypted = Buffer.concat([
        decipher.update(ciphertext),
        decipher.final(),
      ]);

      return decrypted.toString('utf8');
    } catch (error) {
      this.logger.error('Decryption failed - data may be corrupted or tampered with');
      throw new Error('Failed to decrypt data');
    }
  }

  /**
   * Encrypts a JavaScript object as JSON
   *
   * @param data - Object to encrypt
   * @returns Encrypted string
   */
  encryptObject<T>(data: T): string {
    return this.encrypt(JSON.stringify(data));
  }

  /**
   * Decrypts a string back to a JavaScript object
   *
   * @param encryptedData - Encrypted string from encryptObject()
   * @returns The original object
   */
  decryptObject<T>(encryptedData: string): T {
    const json = this.decrypt(encryptedData);
    return JSON.parse(json) as T;
  }

  /**
   * Generates a secure random string suitable for use as an encryption key
   *
   * @param length - Length of the key in bytes (default: 32 for AES-256)
   * @returns Hex-encoded random string
   */
  static generateKey(length: number = 32): string {
    return crypto.randomBytes(length).toString('hex');
  }
}
