import * as crypto from "crypto";

// Encryption configuration
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 16; // 128 bits
const SALT_LENGTH = 32; // 256 bits
const TAG_LENGTH = 16; // 128 bits
const KEY_LENGTH = 32; // 256 bits
const ITERATIONS = 100000; // PBKDF2 iterations

// Get or generate encryption key
function getEncryptionKey(): Buffer {
  const secret = process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET;
  
  if (!secret) {
    throw new Error("No encryption secret found. Please set ENCRYPTION_SECRET environment variable.");
  }
  
  // Use a static salt derived from the secret for consistent key generation
  // This ensures the same key is generated across application restarts
  const salt = crypto.createHash('sha256').update('gitea-mirror-salt' + secret).digest();
  
  return crypto.pbkdf2Sync(secret, salt, ITERATIONS, KEY_LENGTH, 'sha256');
}

export interface EncryptedData {
  encrypted: string;
  iv: string;
  salt: string;
  tag: string;
  version: number;
}

/**
 * Encrypts sensitive data like API tokens
 * @param plaintext The data to encrypt
 * @returns Encrypted data with metadata
 */
export function encrypt(plaintext: string): string {
  if (!plaintext) {
    return '';
  }

  try {
    const key = getEncryptionKey();
    const iv = crypto.randomBytes(IV_LENGTH);
    const salt = crypto.randomBytes(SALT_LENGTH);
    
    const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
    
    const encrypted = Buffer.concat([
      cipher.update(plaintext, 'utf8'),
      cipher.final()
    ]);
    
    const tag = cipher.getAuthTag();
    
    const encryptedData: EncryptedData = {
      encrypted: encrypted.toString('base64'),
      iv: iv.toString('base64'),
      salt: salt.toString('base64'),
      tag: tag.toString('base64'),
      version: 1
    };
    
    // Return as base64 encoded JSON for easy storage
    return Buffer.from(JSON.stringify(encryptedData)).toString('base64');
  } catch (error) {
    console.error('Encryption error:', error);
    throw new Error('Failed to encrypt data');
  }
}

/**
 * Decrypts encrypted data
 * @param encryptedString The encrypted data string
 * @returns Decrypted plaintext
 */
export function decrypt(encryptedString: string): string {
  if (!encryptedString) {
    return '';
  }

  try {
    // Check if it's already plaintext (for backward compatibility during migration)
    if (!isEncrypted(encryptedString)) {
      return encryptedString;
    }
    
    const encryptedData: EncryptedData = JSON.parse(
      Buffer.from(encryptedString, 'base64').toString('utf8')
    );
    
    const key = getEncryptionKey();
    const iv = Buffer.from(encryptedData.iv, 'base64');
    const tag = Buffer.from(encryptedData.tag, 'base64');
    const encrypted = Buffer.from(encryptedData.encrypted, 'base64');
    
    const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
    decipher.setAuthTag(tag);
    
    const decrypted = Buffer.concat([
      decipher.update(encrypted),
      decipher.final()
    ]);
    
    return decrypted.toString('utf8');
  } catch (error) {
    // If decryption fails, check if it's plaintext (backward compatibility)
    try {
      JSON.parse(Buffer.from(encryptedString, 'base64').toString('utf8'));
      throw error; // It was encrypted but failed to decrypt
    } catch {
      // Not encrypted, return as-is for backward compatibility
      console.warn('Token appears to be unencrypted, returning as-is for backward compatibility');
      return encryptedString;
    }
  }
}

/**
 * Checks if a string is encrypted
 * @param value The string to check
 * @returns true if encrypted, false otherwise
 */
export function isEncrypted(value: string): boolean {
  if (!value) {
    return false;
  }
  
  try {
    const decoded = Buffer.from(value, 'base64').toString('utf8');
    const data = JSON.parse(decoded);
    return data.version === 1 && data.encrypted && data.iv && data.tag;
  } catch {
    return false;
  }
}

/**
 * Migrates unencrypted tokens to encrypted format
 * @param token The token to migrate
 * @returns Encrypted token if it wasn't already encrypted
 */
export function migrateToken(token: string): string {
  if (!token || isEncrypted(token)) {
    return token;
  }
  
  return encrypt(token);
}

/**
 * Generates a secure random token
 * @param length Token length in bytes (default: 32)
 * @returns Hex encoded random token
 */
export function generateSecureToken(length: number = 32): string {
  return crypto.randomBytes(length).toString('hex');
}

/**
 * Hashes a value using SHA-256 (for non-reversible values like API keys for comparison)
 * @param value The value to hash
 * @returns Hex encoded hash
 */
export function hashValue(value: string): string {
  return crypto.createHash('sha256').update(value).digest('hex');
}