'use strict';

// Newer Codex CLI versions background-compress cold rollouts to
// `rollout-*.jsonl.zst` and re-materialize the live `.jsonl` on resume.
// CTM reads rollouts directly, so a compressed archive must be lazily
// materialized to a disk cache once and then served like any other rollout.
// Compressed rollouts are cold + immutable by definition — no tail/HWM
// semantics needed; the cache key embeds mtime+size so a replaced archive
// re-materializes.

const fs = require('fs');
const os = require('os');
const path = require('path');
const zlib = require('zlib');
const { spawnSync } = require('child_process');

const MAX_COMPRESSED_BYTES = 64 * 1024 * 1024;   // skip absurd archives
const MAX_OUTPUT_BYTES = 512 * 1024 * 1024;       // decompression bomb guard
const CACHE_MAX_FILES = 16;
const CACHE_MAX_BYTES = 1024 * 1024 * 1024;

function isZstRolloutFileName(name) {
  return /\.jsonl\.zst$/i.test(String(name || ''));
}

function cacheDir() {
  if (process.env.CTM_CODEX_ZST_CACHE_DIR) return process.env.CTM_CODEX_ZST_CACHE_DIR;
  return path.join(os.homedir(), '.walle', 'cache', 'codex-zst');
}

let _capability = null;
function zstdCapability() {
  if (_capability) return _capability;
  if (typeof zlib.zstdDecompressSync === 'function') { _capability = 'zlib'; return _capability; }
  try {
    const probe = spawnSync('zstd', ['--version'], { stdio: 'ignore', timeout: 5000 });
    if (probe.status === 0) { _capability = 'binary'; return _capability; }
  } catch { /* not on PATH */ }
  _capability = 'none';
  return _capability;
}

function _decompress(buf) {
  const cap = zstdCapability();
  if (cap === 'zlib') return zlib.zstdDecompressSync(buf, { maxOutputLength: MAX_OUTPUT_BYTES });
  if (cap === 'binary') {
    const run = spawnSync('zstd', ['-dc'], { input: buf, maxBuffer: MAX_OUTPUT_BYTES, timeout: 120000 });
    if (run.status === 0 && run.stdout) return run.stdout;
    throw new Error(`zstd -dc failed: ${String(run.stderr || run.error || 'unknown').slice(0, 200)}`);
  }
  const err = new Error('no zstd decompressor (Node >= 22.15 zlib.zstdDecompressSync or the zstd binary required)');
  err.code = 'NO_ZSTD';
  throw err;
}

function _pruneCache(dir) {
  let entries = [];
  try {
    entries = fs.readdirSync(dir)
      .filter(name => name.endsWith('.jsonl'))
      .map(name => {
        const fullPath = path.join(dir, name);
        try { const st = fs.statSync(fullPath); return { fullPath, size: st.size, mtimeMs: st.mtimeMs }; }
        catch { return null; }
      })
      .filter(Boolean)
      .sort((a, b) => b.mtimeMs - a.mtimeMs);
  } catch { return; }
  let totalBytes = 0;
  entries.forEach((entry, idx) => {
    totalBytes += entry.size;
    if (idx >= CACHE_MAX_FILES || totalBytes > CACHE_MAX_BYTES) {
      try { fs.unlinkSync(entry.fullPath); } catch { /* concurrent prune */ }
    }
  });
}

// `.zst` → cached decompressed `.jsonl` path; anything else passes through
// unchanged so callers can call this unconditionally.
function materializeCodexRollout(filePath) {
  const p = String(filePath || '');
  if (!isZstRolloutFileName(p)) return { path: p, ok: true, reason: '' };
  let st;
  try { st = fs.statSync(p); } catch { return { path: '', ok: false, reason: 'stat_failed' }; }
  if (st.size > MAX_COMPRESSED_BYTES) return { path: '', ok: false, reason: 'too_large' };
  if (zstdCapability() === 'none') return { path: '', ok: false, reason: 'no_zstd' };
  const dir = cacheDir();
  const key = `${path.basename(p).replace(/\.zst$/i, '').replace(/\.jsonl$/i, '')}-${Math.round(st.mtimeMs)}-${st.size}.jsonl`;
  const out = path.join(dir, key);
  try { if (fs.existsSync(out)) return { path: out, ok: true, reason: 'cache' }; } catch { /* fall through */ }
  try {
    fs.mkdirSync(dir, { recursive: true });
    const decompressed = _decompress(fs.readFileSync(p));
    const tmp = `${out}.tmp-${process.pid}`;
    fs.writeFileSync(tmp, decompressed);
    fs.renameSync(tmp, out);
    _pruneCache(dir);
    return { path: out, ok: true, reason: 'decompressed' };
  } catch (e) {
    return { path: '', ok: false, reason: `decompress_failed:${String(e.code || e.message || '').slice(0, 80)}` };
  }
}

// Drop a `.zst` archive when its live `.jsonl` sibling exists (the
// resume-rematerialization case — the live file is authoritative).
function dropZstWithLiveSibling(filePaths) {
  const list = Array.isArray(filePaths) ? filePaths : [];
  const inSet = new Set(list);
  return list.filter(p => {
    if (!isZstRolloutFileName(p)) return true;
    const live = p.replace(/\.zst$/i, '');
    if (inSet.has(live)) return false;
    try { return !fs.existsSync(live); } catch { return true; }
  });
}

module.exports = {
  isZstRolloutFileName,
  zstdCapability,
  materializeCodexRollout,
  dropZstWithLiveSibling,
  MAX_COMPRESSED_BYTES,
  _pruneCache,
};
