/**
 * API Key Manager - Persistent API key storage
 *
 * Purpose:
 * - Store API keys with encrypted persistence to disk
 * - Manage both platform (Loxia) and vendor-specific keys
 * - Provide keys to AI services based on model requirements
 * - Survive backend restarts by loading persisted keys
 *
 * Note: API keys are stored globally (not per-session) since there's
 * only one user using the local backend. Session IDs are ephemeral
 * and change on every restart, so session-based storage doesn't make sense.
 */

import { promises as fs } from 'fs';
import path from 'path';
import { getUserDataPaths, ensureUserDataDirs } from '../utilities/userDataDir.js';
import { isPlaceholderApiKey } from '../utilities/apiKeyPlaceholders.js';
import {
  getOrCreateEncryptionKey as _getOrCreateEncryptionKey,
  encrypt as _cipherEncrypt,
  decrypt as _cipherDecrypt,
} from '../utilities/secretsCipher.js';

class ApiKeyManager {
  constructor(logger) {
    this.logger = logger;

    // Global API keys (persisted)
    this.keys = {
      loxiaApiKey: null,
      vendorKeys: {}
    };

    // Persistence configuration
    this.persistenceFile = null;
    this.encryptionKey = null;
    this.initialized = false;
  }

  /**
   * Initialize persistence - must be called before using persistence features
   * @returns {Promise<void>}
   */
  async initialize() {
    if (this.initialized) return;

    try {
      await ensureUserDataDirs();
      const paths = getUserDataPaths();
      this.persistenceFile = path.join(paths.settings, 'api-keys.enc');

      // Generate or load machine-specific encryption key
      this.encryptionKey = await this._getOrCreateEncryptionKey(paths.settings);

      // Load persisted keys
      await this.loadFromDisk();

      this.initialized = true;
      this.logger?.info('[ApiKeyManager] Initialized with persistence', {
        hasLoxiaKey: !!this.keys.loxiaApiKey
      });
    } catch (error) {
      this.logger?.warn('[ApiKeyManager] Persistence initialization failed, using memory-only mode', {
        error: error.message
      });
    }
  }

  /**
   * Cipher primitives delegate to `utilities/secretsCipher.js` so the
   * api-keys.enc and auth-cache.enc stores share one implementation
   * and one byte layout. See that module for the on-disk format and
   * the KEY_DERIVATION_DOMAIN constant.
   */
  async _getOrCreateEncryptionKey(settingsDir) {
    return _getOrCreateEncryptionKey(settingsDir, this.logger);
  }

  _encrypt(plaintext) {
    return _cipherEncrypt(plaintext, this.encryptionKey);
  }

  _decrypt(encryptedData) {
    return _cipherDecrypt(encryptedData, this.encryptionKey);
  }

  /**
   * Persist keys to encrypted file
   * @returns {Promise<void>}
   */
  async persist() {
    if (!this.persistenceFile || !this.encryptionKey) {
      return; // Persistence not initialized
    }

    try {
      const data = {
        version: 2,
        savedAt: new Date().toISOString(),
        keys: this.keys
      };

      const encrypted = this._encrypt(JSON.stringify(data));
      await fs.writeFile(this.persistenceFile, encrypted, { mode: 0o600 });

      this.logger?.debug('[ApiKeyManager] Keys persisted to disk');
    } catch (error) {
      this.logger?.warn('[ApiKeyManager] Failed to persist keys', { error: error.message });
    }
  }

  /**
   * Load persisted keys from disk
   * @returns {Promise<void>}
   */
  async loadFromDisk() {
    if (!this.persistenceFile || !this.encryptionKey) {
      return; // Persistence not initialized
    }

    try {
      const encrypted = await fs.readFile(this.persistenceFile, 'utf8');
      const decrypted = this._decrypt(encrypted);
      const data = JSON.parse(decrypted);

      // Handle both v1 (session-based) and v2 (global) formats
      if (data.version === 2 && data.keys) {
        // New format: direct global keys
        this.keys = data.keys;
      } else if (data.globalKeys) {
        // Old format: migrate from globalKeys
        this.keys = data.globalKeys;
      }

      // Scrub any UI placeholder strings that a buggy older build may
      // have written to disk before the API-key sanitiser was added.
      // Without this, a poisoned persist file keeps 401-ing every agent
      // request even after the UI fix is deployed.
      let scrubbed = false;
      if (isPlaceholderApiKey(this.keys?.loxiaApiKey)) {
        this.logger?.warn('[ApiKeyManager] Scrubbing placeholder loxiaApiKey from persisted store');
        this.keys.loxiaApiKey = null;
        scrubbed = true;
      }
      if (this.keys?.vendorKeys && typeof this.keys.vendorKeys === 'object') {
        for (const k of Object.keys(this.keys.vendorKeys)) {
          if (isPlaceholderApiKey(this.keys.vendorKeys[k])) {
            this.logger?.warn(`[ApiKeyManager] Scrubbing placeholder vendor key: ${k}`);
            delete this.keys.vendorKeys[k];
            scrubbed = true;
          }
        }
      }
      if (scrubbed) {
        // Best-effort re-persist so the scrub takes effect on the next
        // boot too. Failures here aren't fatal — we already have clean
        // in-memory state.
        try { await this.persist(); } catch { /* ok */ }
      }

      this.logger?.info('[ApiKeyManager] Loaded persisted keys', {
        hasLoxiaKey: !!this.keys.loxiaApiKey,
        vendorKeys: Object.keys(this.keys.vendorKeys || {}),
        savedAt: data.savedAt
      });
    } catch (error) {
      if (error.code === 'ENOENT') {
        this.logger?.debug('[ApiKeyManager] No persisted keys found');
      } else {
        this.logger?.warn('[ApiKeyManager] Failed to load persisted keys', {
          error: error.message
        });
      }
    }
  }

  /**
   * Set API keys (sessionId parameter kept for API compatibility but ignored).
   *
   * Defense-in-depth: reject UI placeholder strings on the way in. Earlier
   * the frontend wrote display-only placeholders like
   * `lx_••••••••••••••••(server-managed)` and the `platform-managed`
   * sentinel into the editable settings field, then auto-save echoed them
   * back here — overwriting the real key with the literal placeholder and
   * 401-ing every subsequent agent request. The frontend has been fixed
   * to never write those values, but this guard makes sure a regression
   * (or a third-party UI hitting this endpoint) can't repeat the bug.
   *
   * @param {string} sessionId - Ignored, kept for API compatibility
   * @param {Object} keys - API keys object
   * @param {string} keys.loxiaApiKey - Loxia platform API key
   * @param {Object} keys.vendorKeys - Vendor-specific keys
   */
  async setSessionKeys(sessionId, keys) {
    if (keys.loxiaApiKey) {
      if (isPlaceholderApiKey(keys.loxiaApiKey)) {
        this.logger?.warn('[ApiKeyManager] Refusing to store placeholder string as Loxia API key — keeping existing key', {
          received: keys.loxiaApiKey.slice(0, 16) + '…'
        });
      } else {
        this.keys.loxiaApiKey = keys.loxiaApiKey;
      }
    }
    if (keys.vendorKeys) {
      const cleanVendor = {};
      for (const [vendor, value] of Object.entries(keys.vendorKeys)) {
        if (isPlaceholderApiKey(value)) {
          this.logger?.warn(`[ApiKeyManager] Refusing to store placeholder for vendor ${vendor} — keeping existing key`);
          continue;
        }
        cleanVendor[vendor] = value;
      }
      this.keys.vendorKeys = { ...this.keys.vendorKeys, ...cleanVendor };
    }

    this.logger?.info('[ApiKeyManager] API keys updated', {
      hasLoxiaKey: !!this.keys.loxiaApiKey,
      vendorKeys: Object.keys(this.keys.vendorKeys || {})
    });

    await this.persist();
  }

  /**
   * Get API keys (sessionId parameter kept for API compatibility but ignored)
   * @param {string} sessionId - Ignored, kept for API compatibility
   * @returns {Object} API keys object
   */
  getSessionKeys(sessionId) {
    return this.keys;
  }

  /**
   * Get the appropriate API key for a model/request
   * @param {string} sessionId - Ignored, kept for API compatibility
   * @param {Object} options - Request options
   * @param {boolean} options.platformProvided - Whether this is a platform-provided model
   * @param {string} options.vendor - Vendor name (anthropic, openai, etc.)
   * @returns {Object} { loxiaApiKey, vendorApiKey }
   */
  getKeysForRequest(sessionId, options = {}) {
    const result = {
      loxiaApiKey: this.keys.loxiaApiKey,
      vendorApiKey: null
    };

    // For direct access models, also include vendor-specific key
    if (!options.platformProvided && options.vendor && this.keys.vendorKeys) {
      result.vendorApiKey = this.keys.vendorKeys[options.vendor];
    }

    return result;
  }

  /**
   * Remove API keys (clears all keys)
   * @param {string} sessionId - Ignored, kept for API compatibility
   */
  async removeSessionKeys(sessionId) {
    const hadKeys = !!this.keys.loxiaApiKey;
    this.keys = {
      loxiaApiKey: null,
      vendorKeys: {}
    };

    if (hadKeys) {
      this.logger?.info('[ApiKeyManager] API keys removed');
      await this.persist();
    }
    return hadKeys;
  }

  /**
   * Set global API keys (same as setSessionKeys, kept for API compatibility)
   * @param {Object} keys - API keys
   */
  async setGlobalKeys(keys) {
    return this.setSessionKeys(null, keys);
  }

  /**
   * Get active sessions info (returns empty for compatibility)
   * @returns {Array}
   */
  getActiveSessions() {
    return [];
  }

  /**
   * Cleanup (no-op, kept for API compatibility)
   */
  cleanupExpiredSessions() {
    return 0;
  }
}

export default ApiKeyManager;
