import { getUserVarlockDir, decryptValue, encryptValue, ensureKey } from './chunk-JE2IMKMO.js';
import { createDebug, parseTtl } from './chunk-QGM3LRUO.js';
import { __name } from './chunk-IBAHCH6O.js';
import fs from 'fs';
import path from 'path';
import { createHash, randomBytes } from 'crypto';

var debug = createDebug("varlock:cache");
var FILE_LOCK_OPTS = { waitMs: 25, timeoutMs: 5e3, staleMs: 3e4 };
var KEY_LOCK_OPTS = { waitMs: 50, timeoutMs: 5 * 6e4, staleMs: 10 * 6e4 };
var MAX_CACHE_KEY_LENGTH = 2048;
var TTL_FOREVER_MS = 100 * 365.25 * 864e5;
function expiryFromTtl(now, ttlMs) {
  return Number.isFinite(ttlMs) ? now + ttlMs : now + TTL_FOREVER_MS;
}
__name(expiryFromTtl, "expiryFromTtl");
function groupKeyPrefix(key) {
  const firstColon = key.indexOf(":");
  if (firstColon < 0) return key;
  const secondColon = key.indexOf(":", firstColon + 1);
  return secondColon >= 0 ? key.slice(0, secondColon) : key.slice(0, firstColon);
}
__name(groupKeyPrefix, "groupKeyPrefix");
function hasInvalidCacheKeyChars(key) {
  for (let i = 0; i < key.length; i++) {
    const code = key.charCodeAt(i);
    if (code < 32 || code === 127) return true;
  }
  return false;
}
__name(hasInvalidCacheKeyChars, "hasInvalidCacheKeyChars");
function assertValidCacheKey(key, label = "cache key") {
  if (typeof key !== "string") {
    throw new Error(`Invalid ${label}: must be a string`);
  }
  if (key.length === 0) {
    throw new Error(`Invalid ${label}: cannot be empty`);
  }
  if (key.length > MAX_CACHE_KEY_LENGTH) {
    throw new Error(`Invalid ${label}: exceeds max length (${MAX_CACHE_KEY_LENGTH})`);
  }
  if (hasInvalidCacheKeyChars(key)) {
    throw new Error(`Invalid ${label}: contains control characters`);
  }
}
__name(assertValidCacheKey, "assertValidCacheKey");
var sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => {
  setTimeout(resolve, ms);
}), "sleep");
async function withDirLock(lockPath, opts, fn) {
  const deadline = Date.now() + opts.timeoutMs;
  while (true) {
    try {
      fs.mkdirSync(lockPath, { mode: 448 });
      break;
    } catch (err) {
      if (err?.code === "ENOENT") {
        fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 448 });
        continue;
      }
      if (err?.code !== "EEXIST") {
        throw err;
      }
      try {
        const stat = fs.statSync(lockPath);
        if (Date.now() - stat.mtimeMs > opts.staleMs) {
          const graveyard = `${lockPath}.stale.${process.pid}.${randomBytes(4).toString("hex")}`;
          fs.renameSync(lockPath, graveyard);
          fs.rmSync(graveyard, { recursive: true, force: true });
          continue;
        }
      } catch {
      }
      if (Date.now() >= deadline) {
        throw new Error(`Timed out waiting for cache lock at ${lockPath}`);
      }
      await sleep(opts.waitMs);
    }
  }
  const touchTimer = setInterval(() => {
    try {
      const now = /* @__PURE__ */ new Date();
      fs.utimesSync(lockPath, now, now);
    } catch {
    }
  }, Math.max(1e3, Math.floor(opts.staleMs / 3)));
  touchTimer.unref?.();
  try {
    return await fn();
  } finally {
    clearInterval(touchTimer);
    try {
      fs.rmSync(lockPath, { recursive: true, force: true });
    } catch {
    }
  }
}
__name(withDirLock, "withDirLock");
var CacheStore = class _CacheStore {
  static {
    __name(this, "CacheStore");
  }
  filePath;
  codec;
  static warnedWriteFailure = false;
  constructor(keyId = "varlock-default", codec) {
    const cacheDir = path.join(getUserVarlockDir(), "cache");
    this.filePath = path.join(cacheDir, `${keyId}.json`);
    this.codec = codec ?? {
      ensureReady: /* @__PURE__ */ __name(() => ensureKey(keyId), "ensureReady"),
      encrypt: /* @__PURE__ */ __name((plaintext) => encryptValue(plaintext, keyId), "encrypt"),
      decrypt: /* @__PURE__ */ __name((ciphertext) => decryptValue(ciphertext, keyId), "decrypt")
    };
  }
  /**
   * Load and return a cached value, or undefined on miss/expired/error.
   * The value is JSON-parsed after decryption to preserve its original type (number, boolean, object, etc.).
   */
  async get(cacheKey) {
    assertValidCacheKey(cacheKey);
    const data = this.loadFile();
    const entry = data[cacheKey];
    if (!entry) return void 0;
    if (Date.now() > entry.e) {
      debug("cache expired for %s", cacheKey);
      await this.bestEffortDelete(cacheKey);
      return void 0;
    }
    try {
      const plaintext = await this.codec.decrypt(entry.v);
      const envelope = JSON.parse(plaintext);
      if (!envelope || typeof envelope !== "object" || envelope.k !== cacheKey) {
        debug("cache entry key mismatch for %s", cacheKey);
        await this.bestEffortDelete(cacheKey);
        return void 0;
      }
      return { value: envelope.v, cachedAt: entry.c, expiresAt: entry.e };
    } catch (err) {
      debug("cache decrypt failed for %s: %O", cacheKey, err);
      await this.bestEffortDelete(cacheKey);
      return void 0;
    }
  }
  /**
   * Atomically get a cache entry or compute+store it once per key.
   *
   * Uses a per-key lock so concurrent callers (including across processes)
   * don't stampede the producer for the same cache key.
   */
  async getOrSet(cacheKey, ttlMs, producer) {
    assertValidCacheKey(cacheKey);
    const existing = await this.get(cacheKey);
    if (existing) {
      return { ...existing, cacheHit: true };
    }
    const keyHash = createHash("sha256").update(cacheKey).digest("hex");
    const lockPath = path.join(`${this.filePath}.keylocks`, `${keyHash}.lock`);
    return await withDirLock(lockPath, KEY_LOCK_OPTS, async () => {
      const latest = await this.get(cacheKey);
      if (latest) {
        return { ...latest, cacheHit: true };
      }
      const value = await producer();
      if (value === void 0) return void 0;
      const stored = await this.set(cacheKey, value, ttlMs);
      if (stored) {
        return { value, ...stored, cacheHit: false };
      }
      const now = Date.now();
      return {
        value,
        cachedAt: now,
        expiresAt: expiryFromTtl(now, ttlMs),
        cacheHit: false
      };
    });
  }
  /**
   * Encrypt and store a value with a TTL.
   * The value is JSON-stringified before encryption to preserve its type on retrieval.
   * Returns the stored timestamps, or undefined if the write failed (caching is best-effort).
   */
  async set(cacheKey, value, ttlMs) {
    assertValidCacheKey(cacheKey);
    const now = Date.now();
    const expiresAt = expiryFromTtl(now, ttlMs);
    try {
      await this.codec.ensureReady();
      const serialized = JSON.stringify({ k: cacheKey, v: value });
      const encrypted = await this.codec.encrypt(serialized);
      await this.withFileLock(() => {
        const data = this.pruneExpired(this.loadFile());
        data[cacheKey] = { v: encrypted, c: now, e: expiresAt };
        this.saveFile(data);
      });
      debug("cache set %s (ttl=%dms)", cacheKey, ttlMs);
      return { cachedAt: now, expiresAt };
    } catch (err) {
      debug("cache write failed for %s: %O", cacheKey, err);
      if (!_CacheStore.warnedWriteFailure) {
        _CacheStore.warnedWriteFailure = true;
        console.error("varlock cache write failed \u2014 values will not be cached this run (set DEBUG=varlock:cache for details)");
      }
      return void 0;
    }
  }
  /**
   * Delete a specific cache entry.
   */
  async delete(cacheKey) {
    assertValidCacheKey(cacheKey);
    await this.withFileLock(() => {
      const data = this.loadFile();
      if (cacheKey in data) {
        delete data[cacheKey];
        this.saveFile(data);
      }
    });
  }
  /**
   * Clear all cache entries. Returns the count of cleared entries.
   */
  async clearAll() {
    return await this.withFileLock(() => {
      const data = this.loadFile();
      const count = Object.keys(data).length;
      if (count > 0) {
        this.saveFile({});
      }
      return count;
    });
  }
  /**
   * Clear entries matching a key prefix. Returns the count of cleared entries.
   * Example: `clearByPrefix("plugin:1password:")` clears all 1password plugin cache.
   */
  async clearByPrefix(prefix) {
    assertValidCacheKey(prefix, "cache key prefix");
    return await this.withFileLock(() => {
      const data = this.loadFile();
      let count = 0;
      for (const key of Object.keys(data)) {
        if (key.startsWith(prefix)) {
          delete data[key];
          count++;
        }
      }
      if (count > 0) {
        this.saveFile(data);
      }
      return count;
    });
  }
  /**
   * Get cache statistics. `total` includes expired-but-not-yet-pruned entries.
   */
  getStats() {
    const data = this.loadFile();
    const now = Date.now();
    let expired = 0;
    const byPrefix = {};
    for (const [key, entry] of Object.entries(data)) {
      if (now > entry.e) {
        expired++;
        continue;
      }
      byPrefix[groupKeyPrefix(key)] = (byPrefix[groupKeyPrefix(key)] || 0) + 1;
    }
    return {
      total: Object.keys(data).length,
      expired,
      byPrefix
    };
  }
  /**
   * List all non-expired entries with their metadata (for interactive browsing).
   * Values are NOT decrypted — only keys and timestamps are returned.
   */
  listEntries() {
    const data = this.loadFile();
    const now = Date.now();
    return Object.entries(data).filter(([, entry]) => now <= entry.e).map(([key, entry]) => ({ key, cachedAt: entry.c, expiresAt: entry.e }));
  }
  /**
   * Get the file path for this cache store (for display purposes).
   */
  getFilePath() {
    return this.filePath;
  }
  // -- internal --
  loadFile() {
    try {
      if (!fs.existsSync(this.filePath)) {
        return {};
      }
      const raw = fs.readFileSync(this.filePath, "utf-8");
      return JSON.parse(raw);
    } catch (err) {
      debug("cache file load failed: %O", err);
      return {};
    }
  }
  /** Throws on failure — callers decide whether a failed write is fatal. */
  saveFile(data) {
    const tmpPath = `${this.filePath}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
    fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384, flag: "wx" });
    try {
      fs.renameSync(tmpPath, this.filePath);
    } catch (err) {
      fs.rmSync(tmpPath, { force: true });
      throw err;
    }
    fs.chmodSync(this.filePath, 384);
  }
  pruneExpired(data) {
    const now = Date.now();
    for (const key of Object.keys(data)) {
      if (now > data[key].e) delete data[key];
    }
    return data;
  }
  /** Delete used for internal housekeeping (expired/corrupt entries) — must not break reads */
  async bestEffortDelete(cacheKey) {
    try {
      await this.delete(cacheKey);
    } catch (err) {
      debug("cache cleanup delete failed for %s: %O", cacheKey, err);
    }
  }
  async withFileLock(fn) {
    this.ensureCacheDir();
    return await withDirLock(`${this.filePath}.lock`, FILE_LOCK_OPTS, fn);
  }
  ensureCacheDir() {
    const dir = path.dirname(this.filePath);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true, mode: 448 });
    }
    try {
      fs.chmodSync(dir, 448);
    } catch {
    }
  }
};

// src/lib/cache/plugin-cache-accessor.ts
var PluginCacheAccessor = class {
  constructor(pluginName, cacheStore) {
    this.pluginName = pluginName;
    this.cacheStore = cacheStore;
  }
  pluginName;
  cacheStore;
  static {
    __name(this, "PluginCacheAccessor");
  }
  buildKey(key) {
    return `plugin:${this.pluginName}:${key}`;
  }
  async recordCacheHit(cacheKey, cachedAt, expiresAt) {
    try {
      const { getResolutionContext } = await import('./resolution-context-BFS6LYV7.js');
      const ctx = getResolutionContext();
      ctx?.cacheHits.push({ cacheKey, cachedAt, expiresAt });
    } catch {
    }
  }
  async get(key) {
    const cacheKey = this.buildKey(key);
    const result = await this.cacheStore.get(cacheKey);
    if (result) {
      await this.recordCacheHit(cacheKey, result.cachedAt, result.expiresAt);
    }
    return result?.value;
  }
  async getOrSet(key, ttl, producer) {
    const cacheKey = this.buildKey(key);
    const ttlMs = typeof ttl === "string" ? parseTtl(ttl) : ttl;
    const result = await this.cacheStore.getOrSet(cacheKey, ttlMs, producer);
    if (result?.cacheHit) {
      await this.recordCacheHit(cacheKey, result.cachedAt, result.expiresAt);
    }
    return result?.value;
  }
  async set(key, value, ttl) {
    const ttlMs = typeof ttl === "string" ? parseTtl(ttl) : ttl;
    await this.cacheStore.set(this.buildKey(key), value, ttlMs);
  }
  delete(key) {
    this.cacheStore.delete(this.buildKey(key));
  }
};

// src/lib/cache/noop-cache-store.ts
var NoopCacheStore = class {
  static {
    __name(this, "NoopCacheStore");
  }
  async get() {
    return void 0;
  }
  async getOrSet(cacheKey, ttlMs, producer) {
    const value = await producer();
    if (value === void 0) return void 0;
    const now = Date.now();
    return {
      value,
      cachedAt: now,
      expiresAt: expiryFromTtl(now, ttlMs),
      cacheHit: false
    };
  }
  async set() {
    return void 0;
  }
  async delete() {
  }
  async clearAll() {
    return 0;
  }
};

export { CacheStore, MAX_CACHE_KEY_LENGTH, NoopCacheStore, PluginCacheAccessor, assertValidCacheKey, expiryFromTtl, groupKeyPrefix, hasInvalidCacheKeyChars };
//# sourceMappingURL=chunk-Y2ROUSC3.js.map
//# sourceMappingURL=chunk-Y2ROUSC3.js.map