'use strict';

const fs = require('fs');
const fsp = fs.promises;
const os = require('os');
const path = require('path');

// Transient per-session scrollback snapshots stored as LOCAL files — a
// "hibernation" hand-off, not a durable store.
//
// Scrollback is purely a visual terminal-repaint cache: the agent resumes
// independently via `--resume`; scrollback is only replayed to the browser xterm
// after a CTM *server* restart. So instead of streaming every chunk into the
// shared SQLite write lock (~1 write/sec/active-session — the last high-frequency
// per-session DB writer and a structural bottleneck at scale), we serialize each
// ACTIVE session's headless-xterm buffer to one file (full overwrite, not append)
// on graceful shutdown + periodically as crash insurance, repaint-and-delete it on
// startup, and delete it when the session closes. Files exist only for active
// sessions, so there is no retention/TTL problem to manage.
//
// Files live on LOCAL disk (default ~/.walle/cache/scrollback), NEVER under
// CTM_DATA_DIR — on the primary that is the Dropbox dir, and syncing transient
// repaint blobs is exactly the churn this change exists to avoid.
//
// Format: one JSON file per session, `<sessionId>.scrollback.json`, holding the
// serialized blob plus a small identity header (agentSessionId/cols/rows/fp/ts) so
// restore can validate the snapshot belongs to the session being restored.
//
// LOCATION: under the CTM data dir (`$CTM_DATA_DIR/scrollback/`, default
// `~/.walle/data/scrollback`). The data dir is already per-instance — the primary
// and each /ctm-dev instance (/tmp/walle-dev-<port>) have distinct CTM_DATA_DIRs —
// so primary and dev never share a scrollback dir and the orphan sweep can't reach
// across instances. (On the primary CTM_DATA_DIR lives in Dropbox, so these files
// sync with the rest of the CTM data; that's intentional per the chosen layout.)

function _defaultDir() {
  // Explicit override wins (used by tests and any deliberate placement).
  const fromEnv = process.env.CTM_SCROLLBACK_DIR;
  if (fromEnv && String(fromEnv).trim()) return path.resolve(String(fromEnv).trim());
  const home = os.homedir() || process.env.HOME || os.tmpdir();
  const dataDir = process.env.CTM_DATA_DIR || path.join(home, '.walle', 'data');
  return path.join(dataDir, 'scrollback');
}

function _safeName(sessionId) {
  // Session ids are uuids/slugs, but guard against path traversal regardless.
  return String(sessionId || '').replace(/[^A-Za-z0-9._-]/g, '_');
}

function createScrollbackSnapshotStore(options = {}) {
  const dir = options.dir ? path.resolve(String(options.dir)) : _defaultDir();
  let _ensured = false;
  let _writeSeq = 0;

  function _ensureDir() {
    if (_ensured) return;
    fs.mkdirSync(dir, { recursive: true });
    _ensured = true;
  }

  async function _ensureDirAsync() {
    if (_ensured) return;
    await fsp.mkdir(dir, { recursive: true });
    _ensured = true;
  }

  // Shared input validation + payload/path prep for write()/writeAsync(). Returns
  // null for invalid input (so both writers reject identically), else the prepared
  // JSON + unique temp path + final target. The `++_writeSeq` runs here so two
  // concurrent writes in one process can't collide on the temp path.
  function _prepareWrite(sessionId, record) {
    if (!sessionId || !record || typeof record.data !== 'string') return null;
    const payload = {
      sessionId: String(sessionId),
      agentSessionId: record.agentSessionId || '',
      cols: Number(record.cols) || 0,
      rows: Number(record.rows) || 0,
      fp: record.fp || '',
      ts: Number(record.ts) || Date.now(),
      data: record.data,
    };
    const target = pathFor(sessionId);
    const tmp = `${target}.${process.pid}.${++_writeSeq}.tmp`;
    return { json: JSON.stringify(payload), tmp, target };
  }

  function snapshotDir() { return dir; }

  function pathFor(sessionId) {
    return path.join(dir, `${_safeName(sessionId)}.scrollback.json`);
  }

  // Atomic full-overwrite write (temp + rename). `record` carries the serialized
  // blob and identity header. Returns true on success, false on any fs error
  // (best-effort — a failed snapshot must never break shutdown or a tick).
  function write(sessionId, record) {
    const prep = _prepareWrite(sessionId, record);
    if (!prep) return false;
    try {
      _ensureDir();
      fs.writeFileSync(prep.tmp, prep.json);
      fs.renameSync(prep.tmp, prep.target);
      return true;
    } catch {
      return false;
    }
  }

  // Async sibling of write() — same atomic temp+rename, but the file I/O runs off
  // the main event loop. The periodic snapshot sweep (every active session, every
  // 30s) is the hot caller; on the primary the snapshot dir lives under CTM_DATA_DIR
  // (Dropbox), so sync writeFileSync/renameSync there stalled the loop on Dropbox
  // I/O latency. JSON.stringify stays synchronous (bounded CPU); only the I/O moves.
  async function writeAsync(sessionId, record) {
    const prep = _prepareWrite(sessionId, record);
    if (!prep) return false;
    try {
      await _ensureDirAsync();
      await fsp.writeFile(prep.tmp, prep.json);
      await fsp.rename(prep.tmp, prep.target);
      return true;
    } catch {
      return false;
    }
  }

  // Read a session's snapshot record, or null if absent/unreadable/corrupt.
  function read(sessionId) {
    if (!sessionId) return null;
    try {
      const raw = fs.readFileSync(pathFor(sessionId), 'utf8');
      const rec = JSON.parse(raw);
      if (!rec || typeof rec.data !== 'string') return null;
      return rec;
    } catch {
      return null;
    }
  }

  function remove(sessionId) {
    if (!sessionId) return;
    try { fs.unlinkSync(pathFor(sessionId)); } catch { /* already gone */ }
  }

  // Session ids that currently have a snapshot file on disk (for orphan cleanup).
  function listSessionIds() {
    try {
      return fs.readdirSync(dir)
        .filter((f) => f.endsWith('.scrollback.json'))
        .map((f) => f.slice(0, -'.scrollback.json'.length));
    } catch {
      return [];
    }
  }

  return { write, writeAsync, read, remove, listSessionIds, pathFor, snapshotDir };
}

module.exports = { createScrollbackSnapshotStore };
