/**
 * Credential Manager for DashCaddy
 * Unified interface for secure credential storage
 * Uses OS keychain when available, falls back to encrypted file storage
 */

const keychainManager = require('../security/keychain-manager');
const cryptoUtils = require('../security/crypto-utils');
const lockfile = require('proper-lockfile');
const fs = require('fs');
const path = require('path');

// Resolve credentials file path — supports both standard install (/app/credentials.json)
// and custom deployments with consolidated data directory (/app/data/credentials.json)
function resolveCredentialsFile() {
  if (process.env.CREDENTIALS_FILE) {
    return process.env.CREDENTIALS_FILE;
  }
  const candidates = [
    path.join(__dirname, 'credentials.json'),
    path.join(__dirname, 'data', 'credentials.json'),
  ];
  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }
  // No existing file — return standard path so first store() creates it there
  return candidates[0];
}

const CREDENTIALS_FILE = resolveCredentialsFile();

class CredentialManager {
  constructor() {
    this.useKeychain = keychainManager.available;
    this.cache = new Map(); // In-memory cache with TTL
    this.CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
    this.lockOptions = {
      retries: { retries: 10, minTimeout: 100, maxTimeout: 300 },
      stale: 30000
    };

    console.log(`[CredentialManager] Initialized with ${this.useKeychain ? 'OS keychain' : 'encrypted file'} storage`);
  }

  /**
   * Store a credential securely
   * @param {string} key - Credential identifier (e.g., 'dns.token', 'cloudflare.apikey')
   * @param {string} value - Credential value
   * @param {Object} metadata - Optional metadata (non-sensitive)
   * @returns {Promise<boolean>} Success status
   */
  async store(key, value, metadata = {}) {
    try {
      // Validate inputs
      if (!key || typeof key !== 'string') {
        throw new Error('Credential key is required');
      }
      if (!value || typeof value !== 'string') {
        throw new Error('Credential value is required');
      }

      // Try OS keychain first
      if (this.useKeychain) {
        const success = await keychainManager.store(key, value);
        if (success) {
          // Store metadata separately in file
          await this.storeMetadata(key, metadata);
          this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
          console.log(`[CredentialManager] Stored '${key}' in OS keychain`);
          return true;
        }
        console.warn(`[CredentialManager] Keychain storage failed for '${key}', falling back to encrypted file`);
      }

      // Fallback to encrypted file storage
      await this.storeInFile(key, value, metadata);
      this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
      console.log(`[CredentialManager] Stored '${key}' in encrypted file`);
      return true;
    } catch (error) {
      console.error(`[CredentialManager] Failed to store '${key}':`, error.message);
      return false;
    }
  }

  /**
   * Retrieve a credential
   * @param {string} key - Credential identifier
   * @returns {Promise<string|null>} Credential value or null
   */
  async retrieve(key) {
    try {
      // Check cache first (with TTL expiration)
      if (this.cache.has(key)) {
        const cached = this.cache.get(key);
        if (Date.now() < cached.exp) {
          return cached.value;
        }
        this.cache.delete(key);
      }

      // Try OS keychain first
      if (this.useKeychain) {
        const value = await keychainManager.retrieve(key);
        if (value) {
          this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
          return value;
        }
      }

      // Fallback to encrypted file storage
      const value = await this.retrieveFromFile(key);
      if (value) {
        this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
      }
      return value;
    } catch (error) {
      console.error(`[CredentialManager] Failed to retrieve '${key}':`, error.message);
      return null;
    }
  }

  /**
   * Delete a credential
   * @param {string} key - Credential identifier
   * @returns {Promise<boolean>} Success status
   */
  async delete(key) {
    try {
      // Remove from cache
      this.cache.delete(key);

      // Try OS keychain
      if (this.useKeychain) {
        await keychainManager.delete(key);
      }

      // Remove from file storage
      await this.deleteFromFile(key);
      
      console.log(`[CredentialManager] Deleted '${key}'`);
      return true;
    } catch (error) {
      console.error(`[CredentialManager] Failed to delete '${key}':`, error.message);
      return false;
    }
  }

  /**
   * List all stored credential keys (not values)
   * @returns {Promise<Array<string>>} Array of credential keys
   */
  async list() {
    try {
      const credentials = await this.loadCredentialsFile();
      return Object.keys(credentials);
    } catch (error) {
      console.error('[CredentialManager] Failed to list credentials:', error.message);
      return [];
    }
  }

  /**
   * Get metadata for a credential
   * @param {string} key - Credential identifier
   * @returns {Promise<Object|null>} Metadata object or null
   */
  async getMetadata(key) {
    try {
      const credentials = await this.loadCredentialsFile();
      return credentials[key]?.metadata || null;
    } catch (error) {
      return null;
    }
  }

  /**
   * Rotate encryption key (re-encrypt all credentials with new key)
   * @returns {Promise<boolean>} Success status
   */
  async rotateEncryptionKey() {
    let release;
    try {
      console.log('[CredentialManager] Starting encryption key rotation...');

      // Ensure file exists before locking
      this._ensureFileExists();
      release = await lockfile.lock(CREDENTIALS_FILE, this.lockOptions);

      const data = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
      const credentials = JSON.parse(data);
      const keys = Object.keys(credentials);

      if (keys.length === 0) {
        console.log('[CredentialManager] No credentials to rotate');
        return true;
      }

      // Decrypt all values with the CURRENT key first
      const decryptedEntries = {};
      for (const key of keys) {
        const value = credentials[key].value;
        decryptedEntries[key] = {
          plaintext: cryptoUtils.isEncrypted(value) ? cryptoUtils.decrypt(value) : value,
          metadata: credentials[key].metadata
        };
      }

      // Generate new key (this replaces the cached key and saves to disk)
      const { oldKey } = cryptoUtils.rotateKey();

      // Re-encrypt all credentials with the new key
      const rotated = {};
      for (const key of keys) {
        rotated[key] = {
          value: cryptoUtils.encrypt(decryptedEntries[key].plaintext),
          metadata: decryptedEntries[key].metadata,
          rotatedAt: new Date().toISOString()
        };
      }

      // Save with new encryption
      fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(rotated, null, 2), { mode: 0o600 });

      // Clear cache to force reload
      this.cache.clear();

      console.log(`[CredentialManager] Successfully rotated ${keys.length} credentials`);
      return true;
    } catch (error) {
      console.error('[CredentialManager] Key rotation failed:', error.message);
      return false;
    } finally {
      if (release) {
        try { await release(); } catch (e) { /* lock will expire via stale timeout */ }
      }
    }
  }

  /**
   * Migrate plaintext credentials to encrypted format
   * @returns {Promise<Object>} Migration results
   */
  async migrateToEncrypted() {
    try {
      let migrated = 0;
      let skipped = 0;

      await this._lockedUpdate(credentials => {
        for (const [key, data] of Object.entries(credentials)) {
          if (!cryptoUtils.isEncrypted(data.value)) {
            credentials[key].value = cryptoUtils.encrypt(data.value);
            credentials[key].migratedAt = new Date().toISOString();
            migrated++;
          } else {
            skipped++;
          }
        }
        return credentials;
      });

      if (migrated > 0) {
        this.cache.clear();
        console.log(`[CredentialManager] Migrated ${migrated} plaintext credentials to encrypted format`);
      }

      return { migrated, skipped, total: migrated + skipped };
    } catch (error) {
      console.error('[CredentialManager] Migration failed:', error.message);
      throw error;
    }
  }

  // Private methods

  /**
   * Ensure credentials file exists (needed before locking)
   * @private
   */
  _ensureFileExists() {
    if (!fs.existsSync(CREDENTIALS_FILE)) {
      const dir = path.dirname(CREDENTIALS_FILE);
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
      }
      fs.writeFileSync(CREDENTIALS_FILE, '{}', { mode: 0o600 });
    }
  }

  /**
   * Atomic read-modify-write with file locking
   * @param {Function} updateFn - Receives current credentials object, returns updated object
   * @returns {Promise<Object>} Updated credentials
   * @private
   */
  async _lockedUpdate(updateFn) {
    this._ensureFileExists();
    let release;
    try {
      release = await lockfile.lock(CREDENTIALS_FILE, this.lockOptions);
      const data = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
      const credentials = JSON.parse(data);
      const updated = await updateFn(credentials);
      fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(updated, null, 2), { mode: 0o600 });
      return updated;
    } catch (error) {
      if (error.code === 'ELOCKED') {
        throw new Error('Credentials file is locked by another process. Try again.');
      }
      throw error;
    } finally {
      if (release) {
        try { await release(); } catch (e) { /* lock will expire via stale timeout */ }
      }
    }
  }

  async storeInFile(key, value, metadata) {
    await this._lockedUpdate(credentials => {
      credentials[key] = {
        value: cryptoUtils.encrypt(value),
        metadata,
        updatedAt: new Date().toISOString()
      };
      return credentials;
    });
  }

  async retrieveFromFile(key) {
    const credentials = await this.loadCredentialsFile();
    const data = credentials[key];
    if (!data) return null;

    return cryptoUtils.isEncrypted(data.value)
      ? cryptoUtils.decrypt(data.value)
      : data.value;
  }

  /**
   * Retrieve a credential with diagnostic info on failure.
   *
   * Used by the TOTP recovery flow: when a user is locked out and the secret
   * can't be decrypted (e.g. encryption key was rotated by a container
   * recreate), we need to distinguish "no secret was ever set" from "secret
   * is on disk but unreadable" so the UI can show a useful next step.
   *
   * Status codes:
   *   'ok'                    — value decrypted / returned as-is
   *   'missing'               — key is not present in the store at all
   *   'unreadable'            — key is present but decryption failed (key mismatch / corruption)
   *   'malformed'             — entry exists but value is not in expected encrypted format
   *
   * @param {string} key - Credential identifier
   * @returns {Promise<{ status: string, value: string|null, error?: string }>}
   */
  async diagnose(key) {
    try {
      const credentials = await this.loadCredentialsFile();
      const data = credentials[key];
      if (!data) return { status: 'missing', value: null };

      if (!cryptoUtils.isEncrypted(data.value)) {
        // Plaintext entry — return as-is
        return { status: 'ok', value: data.value };
      }

      try {
        const decrypted = cryptoUtils.decrypt(data.value);
        return { status: 'ok', value: decrypted };
      } catch (decryptErr) {
        // Most common cause: the encryption key on disk is different from
        // the key that originally encrypted this entry (rotated by a
        // container recreate that didn't preserve CREDENTIALS_FILE env).
        console.warn(
          `[CredentialManager] '${key}' is present but cannot be decrypted ` +
          `(likely encryption-key mismatch): ${decryptErr.message}`
        );
        return { status: 'unreadable', value: null, error: decryptErr.message };
      }
    } catch (err) {
      console.error(`[CredentialManager] diagnose('${key}') failed:`, err.message);
      return { status: 'malformed', value: null, error: err.message };
    }
  }

  async deleteFromFile(key) {
    await this._lockedUpdate(credentials => {
      delete credentials[key];
      return credentials;
    });
  }

  async storeMetadata(key, metadata) {
    await this._lockedUpdate(credentials => {
      if (!credentials[key]) {
        credentials[key] = { metadata };
      } else {
        credentials[key].metadata = metadata;
      }
      credentials[key].updatedAt = new Date().toISOString();
      return credentials;
    });
  }

  async loadCredentialsFile() {
    try {
      if (!fs.existsSync(CREDENTIALS_FILE)) {
        return {};
      }
      const data = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
      return JSON.parse(data);
    } catch (error) {
      console.error('[CredentialManager] Failed to load credentials file:', error.message);
      return {};
    }
  }

  /**
   * Export credentials for backup (encrypted)
   * @returns {Promise<string>} Encrypted backup data
   */
  async exportBackup() {
    const credentials = await this.loadCredentialsFile();
    const backup = {
      version: '1.0',
      exportedAt: new Date().toISOString(),
      credentials
    };
    return cryptoUtils.encrypt(JSON.stringify(backup));
  }

  /**
   * Import credentials from backup
   * @param {string} encryptedBackup - Encrypted backup data
   * @returns {Promise<boolean>} Success status
   */
  async importBackup(encryptedBackup) {
    try {
      const decrypted = cryptoUtils.decrypt(encryptedBackup);
      const backup = JSON.parse(decrypted);

      if (backup.version !== '1.0') {
        throw new Error('Unsupported backup version');
      }

      await this._lockedUpdate(() => backup.credentials);
      this.cache.clear();

      console.log('[CredentialManager] Successfully imported backup');
      return true;
    } catch (error) {
      console.error('[CredentialManager] Failed to import backup:', error.message);
      return false;
    }
  }
}

// Export singleton instance
module.exports = new CredentialManager();
