'use strict';

// Synchronous-I/O census — the companion to lib/main-db-census.js for the OTHER half of the
// boot freeze.
//
// THE PROBLEM IT SOLVES: after the DB title hog (rowForCtmId) was fixed, the boot CPU profile
// went 87% IDLE during the freeze. "Idle" in a CPU profile means the main thread is blocked
// in a synchronous I/O SYSCALL — off-CPU, so a CPU sampler structurally CANNOT name it. The
// remaining 20s+ event-loop blocks are synchronous fs/child_process calls in the restore path
// (readFileSync of cold JSONL, readdirSync/realpathSync tree walks, spawnSync git) that no
// CPU profile will ever attribute. This census monkey-patches those sync syscalls (when armed)
// to time them and name the CALL SITE of every slow one — turning "idle" into a ranked list.
//
// SAFETY: OFF by default. HARD RULE: this monkey-patches global fs, so it must NEVER run on
// the primary — it only arms on a dev/staging instance (which sets DEV_CTM_PORT) with
// CTM_FS_CENSUS=1. There is no primary-arming path. When disabled, nothing is patched — zero
// overhead, zero behavior change. Uses performance.now() (no BigInt allocation) and only
// captures a stack for ops slower than STACK_MS, so even when armed the per-call cost is tiny.

const fs = require('fs');
const child_process = require('child_process');
const { performance } = require('perf_hooks');

const STACK_MS = clampNum(process.env.CTM_FS_CENSUS_STACK_MS, 3, 0.1, 60000);
const BUDGET_MS = clampNum(process.env.CTM_FS_CENSUS_BUDGET_MS, 100, 1, 600000);
const MAX_KEYS = clampNum(process.env.CTM_FS_CENSUS_MAX_KEYS, 4000, 16, 100000);

function clampNum(raw, dflt, lo, hi) {
  const n = Number(raw);
  if (!Number.isFinite(n)) return dflt;
  return Math.max(lo, Math.min(hi, n));
}

let _enabled = false;
let _armedReason = '';
let _patched = false;
let _startedAt = 0;
let _budgetWarnings = 0;
const _byFn = new Map();    // fn → { count, sumMs, maxMs }   (every call, cheap)
const _bySite = new Map();  // `fn @ callsite` → { fn, count, sumMs, maxMs, sampleArg, stack } (slow only)

// HARD RULE: this census MONKEY-PATCHES the global fs/child_process sync methods — even more
// invasive than the DB census. It must NEVER run on the primary. It only arms on a dev/staging
// instance (which sets DEV_CTM_PORT). No primary-arming path (no sentinel) — staging only.
function _shouldArm() {
  if (!process.env.DEV_CTM_PORT) return ''; // primary: never arm, full stop
  if (process.env.CTM_FS_CENSUS === '1') return 'env';
  return '';
}

function _captureStack() {
  const raw = new Error().stack || '';
  return raw
    .split('\n')
    .slice(1)
    .filter((l) => !l.includes('sync-io-census.js') && !l.includes('node:internal'))
    .slice(0, 6)
    .map((l) => l.trim())
    .join(' <- ');
}

// Nearest CTM frame in the stack — the line of our code that issued the syscall.
function _ctmCallSite() {
  const raw = new Error().stack || '';
  for (const line of raw.split('\n').slice(1)) {
    if (line.includes('/claude-task-manager/') && !line.includes('/node_modules/') && !line.includes('sync-io-census.js')) {
      return line.trim().replace(/^at\s+/, '').replace(/.*\/claude-task-manager\//, '');
    }
  }
  return '(non-ctm)';
}

function _argHint(arg) {
  if (typeof arg !== 'string') return '';
  // Keep the tail of the path (most informative) without leaking the full home prefix.
  return arg.length > 80 ? '…' + arg.slice(-80) : arg;
}

function _record(fn, ms, firstArg) {
  let fnSt = _byFn.get(fn);
  if (!fnSt) { fnSt = { count: 0, sumMs: 0, maxMs: 0 }; _byFn.set(fn, fnSt); }
  fnSt.count += 1; fnSt.sumMs += ms; if (ms > fnSt.maxMs) fnSt.maxMs = ms;

  if (ms >= STACK_MS) {
    const site = _ctmCallSite();
    const key = `${fn} @ ${site}`;
    let st = _bySite.get(key);
    if (!st) {
      if (_bySite.size >= MAX_KEYS) return;
      st = { fn, site, count: 0, sumMs: 0, maxMs: 0, sampleArg: _argHint(firstArg), stack: '' };
      _bySite.set(key, st);
    }
    st.count += 1; st.sumMs += ms; if (ms > st.maxMs) { st.maxMs = ms; st.stack = _captureStack(); st.sampleArg = _argHint(firstArg); }
    if (ms >= BUDGET_MS) {
      _budgetWarnings += 1;
      console.warn(`[fsio-budget] main-thread ${fn}(${st.sampleArg}) blocked ${Math.round(ms)}ms\n           ${st.stack}`);
    }
  }
}

// Wrap one sync function on a host object, timing every call. Transparent: same args, same
// return, same throw behavior — only a timing side effect.
function _wrap(host, name, label) {
  const orig = host[name];
  if (typeof orig !== 'function') return;
  const wrapped = function (...args) {
    const t0 = performance.now();
    try {
      return orig.apply(this, args);
    } finally {
      _record(label || name, performance.now() - t0, args[0]);
    }
  };
  // Preserve sub-properties (e.g. fs.realpathSync.native).
  for (const k of Object.keys(orig)) { try { wrapped[k] = orig[k]; } catch {} }
  host[name] = wrapped;
  return orig;
}

function arm() {
  if (_enabled) return true;
  const reason = _shouldArm();
  if (!reason) return false;
  _enabled = true;
  _armedReason = reason;
  _startedAt = Date.now();
  if (!_patched) {
    _patched = true;
    for (const n of ['readFileSync', 'readdirSync', 'statSync', 'lstatSync', 'existsSync', 'realpathSync', 'writeFileSync', 'readlinkSync', 'openSync']) {
      _wrap(fs, n);
    }
    // realpathSync.native is a hot path (it's its own function on the wrapper now).
    try { if (fs.realpathSync && typeof fs.realpathSync.native === 'function') _wrap(fs.realpathSync, 'native', 'realpathSync.native'); } catch {}
    for (const n of ['spawnSync', 'execSync', 'execFileSync']) { _wrap(child_process, n); }
  }
  console.warn(`[fsio-census] ARMED (${reason}) — timing synchronous fs/child_process syscalls; `
    + `budget=${BUDGET_MS}ms stackThreshold=${STACK_MS}ms. GET /api/diagnostics/fs-census to read.`);
  return true;
}

function getCensus({ limit = 60 } = {}) {
  const sites = [..._bySite.values()]
    .map((s) => ({ ...s, sumMs: +s.sumMs.toFixed(1), maxMs: +s.maxMs.toFixed(1) }))
    .sort((a, b) => b.sumMs - a.sumMs)
    .slice(0, Math.max(1, limit));
  const byFn = [..._byFn.entries()]
    .map(([fn, s]) => ({ fn, count: s.count, sumMs: +s.sumMs.toFixed(1), maxMs: +s.maxMs.toFixed(1) }))
    .sort((a, b) => b.sumMs - a.sumMs);
  return {
    enabled: _enabled,
    armedReason: _armedReason,
    sinceMs: _startedAt ? Date.now() - _startedAt : 0,
    budgetMs: BUDGET_MS,
    stackThresholdMs: STACK_MS,
    budgetWarnings: _budgetWarnings,
    byFn,
    topSites: sites,
  };
}

function isEnabled() { return _enabled; }

module.exports = { arm, getCensus, isEnabled };
