/**
 * Crypto Utilities for DashCaddy
 * Handles encryption/decryption of sensitive credentials
 * Uses AES-256-GCM for authenticated encryption
 */

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

// Encryption settings
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32; // 256 bits
const IV_LENGTH = 16;  // 128 bits for GCM
const AUTH_TAG_LENGTH = 16;
const SALT_LENGTH = 32;

// Key file location (should be outside of mounted volumes for security)
const KEY_FILE = process.env.ENCRYPTION_KEY_FILE || path.join(__dirname, '.encryption-key');

let encryptionKey = null;

/**
 * Generate a new encryption key
 * @returns {Buffer} 32-byte encryption key
 */
function generateKey() {
  return crypto.randomBytes(KEY_LENGTH);
}

/**
 * Derive a key from a password using PBKDF2 (async, non-blocking)
 * @param {string} password - Password to derive key from
 * @param {Buffer} salt - Salt for key derivation
 * @returns {Promise<Buffer>} Derived key
 */
async function deriveKey(password, salt) {
  return new Promise((resolve, reject) => {
    crypto.pbkdf2(password, salt, 100000, KEY_LENGTH, 'sha512', (err, key) => {
      if (err) reject(err);
      else resolve(key);
    });
  });
}

/**
 * Load or create the encryption key
 * @returns {Buffer} The encryption key
 */
function loadOrCreateKey() {
  if (encryptionKey) {
    return encryptionKey;
  }

  // Check for key in environment variable first
  if (process.env.DASHCADDY_ENCRYPTION_KEY) {
    encryptionKey = Buffer.from(process.env.DASHCADDY_ENCRYPTION_KEY, 'hex');
    console.log('[Crypto] Using encryption key from environment variable');
    return encryptionKey;
  }

  // Try to load from file
  if (fs.existsSync(KEY_FILE)) {
    try {
      const keyData = fs.readFileSync(KEY_FILE, 'utf8').trim();
      if (keyData.length >= 64) {
        encryptionKey = Buffer.from(keyData, 'hex');
        console.log('[Crypto] Loaded encryption key from file');
        return encryptionKey;
      }
      // File exists but key is invalid/empty - will generate new one below
    } catch (error) {
      console.error('[Crypto] Error loading key file:', error.message);
    }
  }

  // Generate new key
  encryptionKey = generateKey();

  try {
    // Save key to file with restricted permissions
    fs.writeFileSync(KEY_FILE, encryptionKey.toString('hex'), { mode: 0o600 });
    console.log('[Crypto] Generated and saved new encryption key');
  } catch (error) {
    console.warn('[Crypto] Could not save key to file:', error.message);
    console.warn('[Crypto] Key will be regenerated on restart - credentials will need to be re-entered');
  }

  return encryptionKey;
}

/**
 * Encrypt sensitive data
 * @param {string|object} data - Data to encrypt (strings or objects)
 * @returns {string} Encrypted data as base64 string with format: iv:authTag:ciphertext
 */
function encrypt(data) {
  const key = loadOrCreateKey();
  const iv = crypto.randomBytes(IV_LENGTH);

  // Convert object to string if needed
  const plaintext = typeof data === 'object' ? JSON.stringify(data) : String(data);

  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);

  let encrypted = cipher.update(plaintext, 'utf8', 'base64');
  encrypted += cipher.final('base64');

  const authTag = cipher.getAuthTag();

  // Return format: iv:authTag:ciphertext (all base64)
  return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
}

/**
 * Decrypt encrypted data
 * @param {string} encryptedData - Encrypted string in format iv:authTag:ciphertext
 * @returns {string} Decrypted plaintext
 */
function decrypt(encryptedData) {
  const key = loadOrCreateKey();

  const parts = encryptedData.split(':');
  if (parts.length !== 3) {
    throw new Error('Invalid encrypted data format');
  }

  const iv = Buffer.from(parts[0], 'base64');
  const authTag = Buffer.from(parts[1], 'base64');
  const ciphertext = parts[2];

  const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

/**
 * Check if a string is encrypted (has our format)
 * @param {string} data - Data to check
 * @returns {boolean} True if data appears to be encrypted
 */
function isEncrypted(data) {
  if (typeof data !== 'string') return false;
  const parts = data.split(':');
  if (parts.length !== 3) return false;

  // Check if parts look like base64
  try {
    Buffer.from(parts[0], 'base64');
    Buffer.from(parts[1], 'base64');
    return true;
  } catch {
    return false;
  }
}

/**
 * Encrypt specific fields in an object
 * @param {object} obj - Object with fields to encrypt
 * @param {string[]} fields - Array of field names to encrypt
 * @returns {object} Object with specified fields encrypted
 */
function encryptFields(obj, fields) {
  const result = { ...obj };
  for (const field of fields) {
    if (result[field] !== undefined && result[field] !== null) {
      // Don't double-encrypt
      if (!isEncrypted(result[field])) {
        result[field] = encrypt(result[field]);
      }
    }
  }
  result._encrypted = true; // Mark as encrypted
  result._encryptedFields = fields;
  return result;
}

/**
 * Decrypt specific fields in an object
 * @param {object} obj - Object with encrypted fields
 * @param {string[]} fields - Array of field names to decrypt (optional, uses _encryptedFields if available)
 * @returns {object} Object with specified fields decrypted
 */
function decryptFields(obj, fields = null) {
  if (!obj._encrypted) {
    return obj; // Not encrypted, return as-is
  }

  const fieldsToDecrypt = fields || obj._encryptedFields || [];
  const result = { ...obj };

  for (const field of fieldsToDecrypt) {
    if (result[field] !== undefined && isEncrypted(result[field])) {
      try {
        result[field] = decrypt(result[field]);
      } catch (error) {
        console.error(`[Crypto] Failed to decrypt field '${field}':`, error.message);
        // Leave the field as-is if decryption fails
      }
    }
  }

  // Remove encryption markers from result
  delete result._encrypted;
  delete result._encryptedFields;

  return result;
}

/**
 * Migrate plaintext credentials to encrypted format
 * @param {object} credentials - Credentials object that may or may not be encrypted
 * @param {string[]} sensitiveFields - Fields that should be encrypted
 * @returns {object} Encrypted credentials object
 */
function migrateToEncrypted(credentials, sensitiveFields) {
  if (credentials._encrypted) {
    return credentials; // Already encrypted
  }

  console.log('[Crypto] Migrating plaintext credentials to encrypted format');
  return encryptFields(credentials, sensitiveFields);
}

/**
 * Read and decrypt a credentials file
 * @param {string} filePath - Path to credentials file
 * @param {string[]} sensitiveFields - Fields that are encrypted
 * @returns {object|null} Decrypted credentials or null if file doesn't exist
 */
function readEncryptedFile(filePath, sensitiveFields = ['password', 'token', 'apiKey', 'secret']) {
  if (!fs.existsSync(filePath)) {
    return null;
  }

  try {
    const data = fs.readFileSync(filePath, 'utf8');
    const parsed = JSON.parse(data);

    // Check if this is encrypted data
    if (parsed._encrypted) {
      return decryptFields(parsed, sensitiveFields);
    }

    // Plain text data - migrate it
    console.log(`[Crypto] Found plaintext data in ${filePath}, will encrypt on next save`);
    return parsed;
  } catch (error) {
    console.error(`[Crypto] Error reading ${filePath}:`, error.message);
    return null;
  }
}

/**
 * Encrypt and write credentials to a file
 * @param {string} filePath - Path to credentials file
 * @param {object} credentials - Credentials to save
 * @param {string[]} sensitiveFields - Fields to encrypt
 */
function writeEncryptedFile(filePath, credentials, sensitiveFields = ['password', 'token', 'apiKey', 'secret']) {
  const encrypted = encryptFields(credentials, sensitiveFields);
  fs.writeFileSync(filePath, JSON.stringify(encrypted, null, 2), 'utf8');
  console.log(`[Crypto] Saved encrypted credentials to ${filePath}`);
}

/**
 * Rotate the encryption key — generates a new key and returns both old and new
 * @returns {{ oldKey: Buffer, newKey: Buffer }} Old and new key pair
 * @throws {Error} If new key cannot be saved to disk
 */
function rotateKey() {
  const oldKey = loadOrCreateKey(); // Ensure we have the current key loaded
  const newKey = generateKey();

  try {
    fs.writeFileSync(KEY_FILE, newKey.toString('hex'), { mode: 0o600 });
  } catch (error) {
    throw new Error(`Failed to save new encryption key: ${error.message}`);
  }

  // Only update the cached key after file write succeeds
  encryptionKey = newKey;
  return { oldKey, newKey };
}

/**
 * Decrypt data using a specific key (for key rotation)
 * @param {string} encryptedData - Encrypted string in format iv:authTag:ciphertext
 * @param {Buffer} key - The key to decrypt with
 * @returns {string} Decrypted plaintext
 */
function decryptWithKey(encryptedData, key) {
  const parts = encryptedData.split(':');
  if (parts.length !== 3) {
    throw new Error('Invalid encrypted data format');
  }

  const iv = Buffer.from(parts[0], 'base64');
  const authTag = Buffer.from(parts[1], 'base64');
  const ciphertext = parts[2];

  const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

// Lazy-initialize: key is loaded on first encrypt/decrypt call.
// Do NOT call loadOrCreateKey() here — during Docker build, it would generate
// a key baked into the image that conflicts with the mounted production key.

/**
 * Clear the cached encryption key so it reloads from file on next use.
 * Called after restoring an encryption key from backup.
 */
function clearCachedKey() {
  encryptionKey = null;
}

module.exports = {
  encrypt,
  decrypt,
  isEncrypted,
  encryptFields,
  decryptFields,
  migrateToEncrypted,
  readEncryptedFile,
  writeEncryptedFile,
  loadOrCreateKey,
  deriveKey,
  rotateKey,
  decryptWithKey,
  clearCachedKey
};
