#!/usr/bin/env node
'use strict';
// Extract a real PTY-byte fixture from CTM's scrollback_log for use in
// rendering tests. Concatenates `data` chunks for a session in chunk_seq
// order, optionally redacts $HOME paths, writes a raw .bin file.
//
// Usage:
//   node bin/extract-fixture.js <session-id> [opts]
//   node bin/extract-fixture.js --list                 # list sessions with byte counts
//
// Options:
//   --from-seq N         Starting chunk_seq (inclusive, default: first)
//   --to-seq N           Ending chunk_seq (inclusive, default: last)
//   --max-bytes N        Stop once N bytes have been written (default: 100000)
//   --out PATH           Output file (default: ./fixture.bin)
//   --redact             Replace $HOME and /Users/<name>/ with /Users/user/
//   --db PATH            Override DB path (default: ~/.walle/data/task-manager.db)
//   --no-trailing        Trim trailing chunk if it would push past --max-bytes (default: trim)

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

function parseArgs(argv) {
  const out = { _: [] };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--list') out.list = true;
    else if (a === '--redact') out.redact = true;
    else if (a === '--no-trailing') out.noTrailing = true;
    else if (a.startsWith('--from-seq')) out.fromSeq = parseInt(a.includes('=') ? a.split('=')[1] : argv[++i], 10);
    else if (a.startsWith('--to-seq')) out.toSeq = parseInt(a.includes('=') ? a.split('=')[1] : argv[++i], 10);
    else if (a.startsWith('--max-bytes')) out.maxBytes = parseInt(a.includes('=') ? a.split('=')[1] : argv[++i], 10);
    else if (a.startsWith('--out')) out.outPath = a.includes('=') ? a.split('=')[1] : argv[++i];
    else if (a.startsWith('--db')) out.dbPath = a.includes('=') ? a.split('=')[1] : argv[++i];
    else if (a.startsWith('--')) { console.error(`unknown option: ${a}`); process.exit(2); }
    else out._.push(a);
  }
  return out;
}

function defaultDbPath() {
  // Mirror DEFAULT_DB_PATH in db.js: ~/.walle/data/task-manager.db
  return path.join(os.homedir(), '.walle', 'data', 'task-manager.db');
}

function redactBytes(buf) {
  // Operate on the string view: data column is TEXT, so it's UTF-8.
  // Replace home-directory paths so fixtures are committable.
  const home = os.homedir();
  let s = buf.toString('utf8');
  // /Users/<anything>/  → /Users/user/
  s = s.replace(/\/Users\/[^/\s'"`)\]]+/g, '/Users/user');
  // Catch unix-style $HOME
  if (home && home !== '/Users/user') s = s.split(home).join('/Users/user');
  return Buffer.from(s, 'utf8');
}

function listSessions(db) {
  const rows = db.prepare(`
    SELECT ctm_session_id, COUNT(*) AS chunks, SUM(LENGTH(data)) AS bytes,
           MIN(chunk_seq) AS first_seq, MAX(chunk_seq) AS last_seq
    FROM scrollback_log
    GROUP BY ctm_session_id
    ORDER BY bytes DESC
  `).all();
  if (!rows.length) { console.error('(no sessions in scrollback_log)'); return; }
  console.log('session_id                            chunks   bytes  seq-range');
  for (const r of rows) {
    console.log(
      `${r.ctm_session_id}  ${String(r.chunks).padStart(6)}  ${String(r.bytes).padStart(7)}  ${r.first_seq}..${r.last_seq}`
    );
  }
}

function extract(db, sessionId, opts) {
  let sql = 'SELECT chunk_seq, data FROM scrollback_log WHERE ctm_session_id = ?';
  const params = [sessionId];
  if (Number.isFinite(opts.fromSeq)) { sql += ' AND chunk_seq >= ?'; params.push(opts.fromSeq); }
  if (Number.isFinite(opts.toSeq)) { sql += ' AND chunk_seq <= ?'; params.push(opts.toSeq); }
  sql += ' ORDER BY chunk_seq ASC';
  const rows = db.prepare(sql).all(...params);
  if (!rows.length) {
    console.error(`no chunks found for session ${sessionId}`);
    process.exit(3);
  }

  const maxBytes = Number.isFinite(opts.maxBytes) ? opts.maxBytes : 100000;
  const parts = [];
  let total = 0;
  let lastSeq = null;
  for (const r of rows) {
    const buf = Buffer.from(r.data, 'utf8');
    if (total + buf.length > maxBytes) {
      if (opts.noTrailing) break;
      // Truncate this chunk to fit the cap
      const remain = maxBytes - total;
      if (remain > 0) parts.push(buf.subarray(0, remain));
      total = maxBytes;
      lastSeq = r.chunk_seq;
      break;
    }
    parts.push(buf);
    total += buf.length;
    lastSeq = r.chunk_seq;
  }

  let out = Buffer.concat(parts, total);
  if (opts.redact) out = redactBytes(out);
  return { bytes: out, lastSeq, firstSeq: rows[0].chunk_seq };
}

function main() {
  const opts = parseArgs(process.argv.slice(2));
  const dbPath = opts.dbPath || defaultDbPath();
  if (!fs.existsSync(dbPath)) {
    console.error(`DB not found: ${dbPath}`);
    process.exit(1);
  }
  const db = new Database(dbPath, { readonly: true, fileMustExist: true });

  if (opts.list) { listSessions(db); db.close(); return; }

  const sessionId = opts._[0];
  if (!sessionId) {
    console.error('usage: extract-fixture.js <session-id> [opts]   (or --list)');
    process.exit(2);
  }

  const result = extract(db, sessionId, opts);
  db.close();

  const outPath = opts.outPath || path.resolve('fixture.bin');
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
  fs.writeFileSync(outPath, result.bytes);
  console.error(
    `wrote ${result.bytes.length} bytes from chunks ${result.firstSeq}..${result.lastSeq} → ${outPath}`
  );
}

main();
