/**
 * Encrypted persistence for `webServer.lastAuthResult`.
 *
 * Purpose:
 * - Survive local-server restarts without losing the OAuth JWT.
 * - Without this, every restart wipes the in-memory auth cache, so
 *   the FE polling fallback at `/api/auth/status` returns
 *   `{ user: null, jwt: null }` and any frontend that didn't already
 *   capture the JWT in its own localStorage (page reload after a
 *   server bounce, fresh browser, incognito tab) gets 401s on every
 *   marketplace publish/install/rate call.
 *
 * Why encrypted:
 * - The cache row carries a raw JWT — a bearer credential. Storing it
 *   in plaintext on disk would let any local process with file-read
 *   access impersonate the user against the marketplace service.
 *
 * Storage: `<userDataDir>/settings/auth-cache.enc`. Cipher and key
 * derivation are shared with `apiKeyManager` via
 * `utilities/secretsCipher.js` — DRY by design.
 */

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

const FILE_NAME = 'auth-cache.enc';
const SCHEMA_VERSION = 1;

class AuthCacheStore {
  constructor(logger) {
    this.logger = logger;
    this.persistenceFile = null;
    this.encryptionKey = null;
    this.initialized = false;
  }

  /**
   * Resolve the encrypted file path and derive (or load) the AES key.
   * Idempotent. Failures are non-fatal — the store then behaves as a
   * no-op (load returns null, save silently swallows), so a broken
   * disk doesn't crash the server boot.
   */
  async initialize() {
    if (this.initialized) return;
    try {
      await ensureUserDataDirs();
      const paths = getUserDataPaths();
      this.persistenceFile = path.join(paths.settings, FILE_NAME);
      this.encryptionKey = await getOrCreateEncryptionKey(paths.settings, this.logger);
      this.initialized = true;
      this.logger?.info?.('[AuthCacheStore] Initialized');
    } catch (error) {
      this.logger?.warn?.('[AuthCacheStore] init failed, running in memory-only mode', {
        error: error.message,
      });
    }
  }

  /**
   * Load and decrypt the persisted cache row.
   * Returns null when the file is missing, corrupted, schema-mismatched,
   * or the store failed to initialize. Never throws.
   *
   * @returns {Promise<Object|null>} the `lastAuthResult`-shaped object
   *   ({ user, hasApiKey, jwt, timestamp }) or null.
   */
  async load() {
    if (!this.initialized) return null;
    try {
      const ciphertext = await fs.readFile(this.persistenceFile, 'utf8');
      const decoded = decrypt(ciphertext, this.encryptionKey);
      const data = JSON.parse(decoded);
      if (data?.version !== SCHEMA_VERSION || !data.cache) {
        this.logger?.warn?.('[AuthCacheStore] schema mismatch, ignoring on-disk cache', {
          version: data?.version,
        });
        return null;
      }
      return data.cache;
    } catch (error) {
      if (error.code !== 'ENOENT') {
        this.logger?.warn?.('[AuthCacheStore] load failed', { error: error.message });
      }
      return null;
    }
  }

  /**
   * Persist a cache row. Passing null clears the file (delegate to
   * clear() for clarity; this is a convenience for the webServer
   * call site that always wants "make disk match memory").
   *
   * @param {Object|null} cache
   */
  async save(cache) {
    if (!this.initialized) return;
    if (cache === null || cache === undefined) {
      await this.clear();
      return;
    }
    try {
      const payload = {
        version: SCHEMA_VERSION,
        savedAt: new Date().toISOString(),
        cache,
      };
      const ciphertext = encrypt(JSON.stringify(payload), this.encryptionKey);
      await fs.writeFile(this.persistenceFile, ciphertext, { mode: 0o600 });
      this.logger?.debug?.('[AuthCacheStore] saved', {
        hasUser: !!cache.user,
        hasJwt: !!cache.jwt,
      });
    } catch (error) {
      this.logger?.warn?.('[AuthCacheStore] save failed', { error: error.message });
    }
  }

  /**
   * Delete the on-disk cache. No-op when the file is already absent.
   */
  async clear() {
    if (!this.initialized) return;
    try {
      await fs.unlink(this.persistenceFile);
      this.logger?.info?.('[AuthCacheStore] cleared');
    } catch (error) {
      if (error.code !== 'ENOENT') {
        this.logger?.warn?.('[AuthCacheStore] clear failed', { error: error.message });
      }
    }
  }
}

export default AuthCacheStore;
