#!/usr/bin/env node
/**
 * OpenClaw Compression — CCR Backends (SQLite + Future Redis)
 *
 * Extends the in-memory CCR Store with persistent backends for
 * multi-process/multi-session shared compression cache.
 *
 * Headroom: SqliteCcrStore + RedisCcrStore (crates/headroom-core/src/ccr/backends/)
 *
 * Usage:
 *   const { SqliteCcrStore, createCcrBackend } = require('./ccr-backends');
 *   const store = createCcrBackend('sqlite', { dbPath: '/tmp/ccr.db' });
 *   store.put(hash, payload);
 */

const Database = require('better-sqlite3');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

// ─── Base Interface ──────────────────────────────────────────────

/**
 * CCR Store interface (pluggable backend):
 *   put(hash, payload)   → boolean
 *   get(hash)            → string | null
 *   has(hash)            → boolean
 *   stats()              → { size, capacity, ... }
 *   clear()              → void
 *   close()              → void (cleanup)
 */

// ─── SQLite CCR Backend ──────────────────────────────────────────

class SqliteCcrStore {
  constructor(options = {}) {
    this.dbPath = options.dbPath || path.join(
      process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, '.openclaw', 'state'),
      'ccr-store.db'
    );
    this.ttlMs = options.ttl || 5 * 60 * 1000;   // 5 min default
    this.capacity = options.capacity || 10000;     // 10K entries
    this.cleanupIntervalMs = options.cleanupIntervalMs || 300000; // 5 min
    this.db = null;
    this._lastCleanup = Date.now();
  }

  _ensureOpen() {
    if (this.db) return;

    const dir = path.dirname(this.dbPath);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

    this.db = new Database(this.dbPath);
    this.db.pragma('journal_mode = WAL');       // Allow concurrent readers
    this.db.pragma('busy_timeout = 5000');      // 5s wait on lock

    this.db.exec(`
      CREATE TABLE IF NOT EXISTS ccr_entries (
        hash TEXT PRIMARY KEY,
        payload TEXT NOT NULL,
        size_bytes INTEGER DEFAULT 0,
        created_at TEXT NOT NULL DEFAULT (datetime('now')),
        expires_at TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_ccr_expires ON ccr_entries(expires_at);
    `);

    // Cleanup expired entries
    this._maybeCleanup();
  }

  put(hash, payload) {
    this._ensureOpen();
    this._maybeCleanup();

    // Check capacity
    const count = this.db.prepare('SELECT COUNT(*) as c FROM ccr_entries').get().c;
    if (count >= this.capacity) {
      // Evict oldest entries (keep 80%)
      const toRemove = Math.ceil(this.capacity * 0.2);
      this.db.prepare(`
        DELETE FROM ccr_entries WHERE hash IN (
          SELECT hash FROM ccr_entries ORDER BY created_at ASC LIMIT ?
        )
      `).run(toRemove);
    }

    const expiresAt = new Date(Date.now() + this.ttlMs).toISOString();
    const sizeBytes = Buffer.byteLength(payload, 'utf8');

    try {
      this.db.prepare(`
        INSERT OR REPLACE INTO ccr_entries (hash, payload, size_bytes, created_at, expires_at)
        VALUES (?, ?, ?, datetime('now'), ?)
      `).run(hash, payload, sizeBytes, expiresAt);
      return true;
    } catch (e) {
      console.error(`CCR SQLite put error: ${e.message}`);
      return false;
    }
  }

  get(hash) {
    this._ensureOpen();
    this._maybeCleanup();

    const row = this.db.prepare(`
      SELECT payload FROM ccr_entries
      WHERE hash = ? AND expires_at > datetime('now')
    `).get(hash);

    return row ? row.payload : null;
  }

  has(hash) {
    return this.get(hash) !== null;
  }

  stats() {
    this._ensureOpen();
    this._maybeCleanup();

    const count = this.db.prepare('SELECT COUNT(*) as c FROM ccr_entries').get().c;
    const totalSize = this.db.prepare('SELECT COALESCE(SUM(size_bytes), 0) as s FROM ccr_entries').get().s;

    return {
      backend: 'sqlite',
      dbPath: this.dbPath,
      entries: count,
      capacity: this.capacity,
      totalSizeBytes: totalSize,
      ttlMs: this.ttlMs,
    };
  }

  clear() {
    this._ensureOpen();
    this.db.prepare('DELETE FROM ccr_entries').run();
  }

  close() {
    if (this.db) {
      this.db.close();
      this.db = null;
    }
  }

  _maybeCleanup() {
    const now = Date.now();
    if (now - this._lastCleanup < this.cleanupIntervalMs) return;
    this._lastCleanup = now;

    this.db.prepare("DELETE FROM ccr_entries WHERE expires_at <= datetime('now')").run();
    // VACUUM to reclaim space (only if many deletions)
    const count = this.db.prepare('SELECT COUNT(*) as c FROM ccr_entries').get().c;
    if (count < this.capacity * 0.3) {
      try { this.db.pragma('incremental_vacuum'); } catch {}
    }
  }
}

// ─── Redis CCR Backend (Stub) ────────────────────────────────────

/**
 * Redis CCR Backend.
 * Requires: npm install ioredis
 * Auto-falls back to in-memory if Redis unavailable.
 */
class RedisCcrStore {
  constructor(options = {}) {
    this.url = options.url || process.env.REDIS_URL || 'redis://localhost:6379';
    this.ttlSeconds = Math.ceil((options.ttl || 300000) / 1000);
    this.keyPrefix = options.keyPrefix || 'ccr:';
    this.redis = null;
    this._fallback = new Map(); // fallback if Redis unavailable
    this._connected = false;
  }

  async _connect() {
    if (this._connected) return true;
    try {
      const Redis = require('ioredis');
      this.redis = new Redis(this.url, {
        lazyConnect: true,
        maxRetriesPerRequest: 2,
        connectTimeout: 3000,
      });
      await this.redis.connect();
      this._connected = true;
      return true;
    } catch {
      return false;
    }
  }

  async put(hash, payload) {
    if (await this._connect()) {
      try {
        await this.redis.setex(
          this.keyPrefix + hash,
          this.ttlSeconds,
          payload
        );
        return true;
      } catch {}
    }

    // Fallback
    this._fallback.set(hash, {
      payload,
      expiresAt: Date.now() + this.ttlSeconds * 1000,
    });
    return true;
  }

  async get(hash) {
    if (this._connected) {
      try {
        return await this.redis.get(this.keyPrefix + hash);
      } catch {}
    }

    // Fallback
    const entry = this._fallback.get(hash);
    if (!entry) return null;
    if (Date.now() > entry.expiresAt) {
      this._fallback.delete(hash);
      return null;
    }
    return entry.payload;
  }

  async has(hash) {
    return (await this.get(hash)) !== null;
  }

  async stats() {
    let redisSize = 0;
    if (this._connected) {
      try { redisSize = await this.redis.dbsize(); } catch {}
    }

    return {
      backend: 'redis',
      url: this.url,
      connected: this._connected,
      redisSize,
      fallbackSize: this._fallback.size,
    };
  }

  async clear() {
    if (this._connected) {
      try {
        const keys = await this.redis.keys(this.keyPrefix + '*');
        if (keys.length > 0) await this.redis.del(...keys);
      } catch {}
    }
    this._fallback.clear();
  }

  async close() {
    if (this.redis) await this.redis.quit();
  }
}

// ─── Factory ─────────────────────────────────────────────────────

/**
 * Create the appropriate CCR backend.
 * @param {'sqlite'|'redis'|'memory'} backend — backend type
 */
function createCcrBackend(backend = 'memory', options = {}) {
  const { CcrStore } = require('./ccr-store');

  switch (backend) {
    case 'sqlite':
      return new SqliteCcrStore(options);
    case 'redis':
      return new RedisCcrStore(options);
    case 'memory':
    default:
      return new CcrStore(options);
  }
}

// ─── Hybrid CCR (tiered: memory → SQLite → Redis) ───────────────

/**
 * Tiered CCR store: L1 memory (fast), L2 SQLite (shared), L3 Redis (distributed).
 * Reads check L1 → L2 → L3. Writes go to all.
 */
class HybridCcrStore {
  constructor(options = {}) {
    this.l1 = createCcrBackend('memory', { capacity: options.l1Capacity || 500, ttl: options.ttl || 60000 });
    this.l2 = createCcrBackend('sqlite', options);
    this.l3 = options.redisUrl ? createCcrBackend('redis', options) : null;
  }

  async put(hash, payload) {
    this.l1.put(hash, payload);
    this.l2.put(hash, payload);
    if (this.l3) await this.l3.put(hash, payload);
    return true;
  }

  async get(hash) {
    // L1 cache
    let result = this.l1.get(hash);
    if (result) return result;

    // L2 SQLite
    result = this.l2.get(hash);
    if (result) {
      this.l1.put(hash, result);  // promote to L1
      return result;
    }

    // L3 Redis
    if (this.l3) {
      result = await this.l3.get(hash);
      if (result) {
        this.l1.put(hash, result);  // promote
        return result;
      }
    }

    return null;
  }

  async has(hash) {
    return (await this.get(hash)) !== null;
  }

  stats() {
    return {
      backend: 'hybrid',
      l1: this.l1.stats(),
      l2: this.l2.stats(),
      l3: this.l3 ? 'redis' : 'none',
    };
  }

  close() {
    if (this.l2.close) this.l2.close();
    if (this.l3?.close) this.l3.close();
  }
}

// ─── Module exports ──────────────────────────────────────────────

module.exports = {
  SqliteCcrStore,
  RedisCcrStore,
  HybridCcrStore,
  createCcrBackend,
};
