'use strict';
/**
 * Structured JSONL logger for error-recovery events.
 *
 * Cross-cutting infrastructure for the error-recovery deep-dive:
 *   - Backoff schedule chosen + duration applied (decideSkillDispatch)
 *   - Per-skill backoff override applied (Item A)
 *   - Credential cooldown applied / expired (Items C + B)
 *   - API key rotation attempt (Item B)
 *   - Channel poll backoff escalated / reset (Item F)
 *   - Tool runtime degradation (Item G)
 *
 * Why a dedicated logger and not just telemetry?
 *   - Telemetry buffers + flushes to a remote endpoint; recovery events
 *     must persist locally for post-incident debugging even when offline
 *   - JSONL on disk is grep-able + jq-able + tail-able from a shell
 *   - Recovery events are noisy enough to flood telemetry batches; better
 *     in their own stream
 *   - Telemetry caps at 500 events; recovery log holds 10MB of history
 *
 * File layout: ${WALL_E_DATA_DIR}/recovery.log (current) +
 *              ${WALL_E_DATA_DIR}/recovery.log.1 (rotated, kept once)
 *
 * Each line is a JSON object:
 *   {
 *     "ts": "2026-04-25T08:30:01.123Z",
 *     "level": "info" | "warn" | "error",
 *     "component": "skill-dispatch" | "key-rotation" | "channel-poll" | ...,
 *     "event": "backoff_applied" | "key_rotated" | ...,
 *     "skill": "...",            // when relevant
 *     "duration_ms": 30000,       // when relevant
 *     "details": { ... }
 *   }
 *
 * Tunables via env:
 *   WALLE_RECOVERY_LOG=0   — disable entirely (still no-throw)
 *   WALLE_RECOVERY_LOG_LEVEL=warn — minimum level to write (default 'info')
 *
 * Async + non-blocking: writes are buffered in memory, flushed on a
 * 1s timer or when buffer >= 64 entries. The daemon never waits on the
 * filesystem.
 */
const fs = require('fs');
const path = require('path');

const LEVEL_PRIORITY = { error: 3, warn: 2, info: 1, debug: 0 };
const ROTATION_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
const FLUSH_INTERVAL_MS = 1000;
const FLUSH_BATCH_SIZE = 64;
const MAX_BUFFER = 4096; // hard cap to prevent unbounded growth

let _buffer = [];
let _flushTimer = null;
let _disabledReason = null;
let _stats = { written: 0, dropped: 0, rotations: 0, errors: 0 };

function _logPath() {
  const dir = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME || '/tmp', '.walle', 'data');
  return path.join(dir, 'recovery.log');
}

function _isDisabled() {
  return process.env.WALLE_RECOVERY_LOG === '0';
}

function _minLevel() {
  const env = process.env.WALLE_RECOVERY_LOG_LEVEL;
  if (env && env in LEVEL_PRIORITY) return env;
  return 'info';
}

function _shouldEmit(level) {
  if (_isDisabled()) return false;
  return (LEVEL_PRIORITY[level] ?? 99) >= LEVEL_PRIORITY[_minLevel()];
}

function _ensureDir(filePath) {
  try {
    const dir = path.dirname(filePath);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  } catch {}
}

function _maybeRotate(filePath) {
  try {
    const stat = fs.statSync(filePath);
    if (stat.size <= ROTATION_SIZE_BYTES) return;
    const rotated = `${filePath}.1`;
    try { fs.rmSync(rotated, { force: true }); } catch {}
    fs.renameSync(filePath, rotated);
    _stats.rotations += 1;
  } catch {
    // statSync throws if file doesn't exist yet — that's fine
  }
}

function _scheduleFlush() {
  if (_flushTimer) return;
  _flushTimer = setTimeout(() => {
    _flushTimer = null;
    _flush();
  }, FLUSH_INTERVAL_MS);
  // Don't keep the daemon alive on this timer
  if (typeof _flushTimer.unref === 'function') _flushTimer.unref();
}

function _flush() {
  if (_buffer.length === 0) return;
  const target = _logPath();
  // Drain the buffer first so concurrent logEvent calls don't double-write
  const lines = _buffer;
  _buffer = [];
  try {
    _ensureDir(target);
    _maybeRotate(target);
    const payload = lines.map(l => JSON.stringify(l)).join('\n') + '\n';
    fs.appendFileSync(target, payload);
    _stats.written += lines.length;
  } catch (err) {
    _stats.errors += 1;
    _disabledReason = err.message;
    // Drop the lines on persistent failure rather than re-buffer (would
    // grow unbounded if disk is full).
    _stats.dropped += lines.length;
  }
}

/**
 * Log a recovery event.
 *
 * @param {Object} entry
 * @param {string} [entry.level='info']
 * @param {string} entry.component
 * @param {string} entry.event
 * @param {Object} [entry.details]
 * @param {string} [entry.skill]
 * @param {string} [entry.profile]
 * @param {string} [entry.channel]
 * @param {number} [entry.duration_ms]
 */
function logEvent(entry) {
  if (!entry || typeof entry !== 'object') return;
  const level = entry.level || 'info';
  if (!_shouldEmit(level)) return;
  if (_buffer.length >= MAX_BUFFER) {
    _stats.dropped += 1;
    return;
  }
  _buffer.push({
    ts: new Date().toISOString(),
    level,
    component: entry.component || 'unknown',
    event: entry.event || 'unknown',
    ...(entry.skill ? { skill: entry.skill } : {}),
    ...(entry.profile ? { profile: entry.profile } : {}),
    ...(entry.channel ? { channel: entry.channel } : {}),
    ...(entry.duration_ms != null ? { duration_ms: entry.duration_ms } : {}),
    ...(entry.details ? { details: entry.details } : {}),
  });
  if (_buffer.length >= FLUSH_BATCH_SIZE) {
    _flush();
  } else {
    _scheduleFlush();
  }
}

// Convenience wrappers
function info(component, event, opts = {}) {
  logEvent({ level: 'info', component, event, ...opts });
}
function warn(component, event, opts = {}) {
  logEvent({ level: 'warn', component, event, ...opts });
}
function error(component, event, opts = {}) {
  logEvent({ level: 'error', component, event, ...opts });
}

/**
 * Read the most recent N entries from the log file (synchronous, used by
 * the API endpoint and tests). Returns an array of parsed JSON objects.
 */
function readRecent(limit = 200) {
  // Flush in-flight buffer first so callers see the latest events
  _flush();
  const target = _logPath();
  if (!fs.existsSync(target)) return [];
  try {
    const content = fs.readFileSync(target, 'utf8');
    const lines = content.split('\n').filter(Boolean);
    const tail = lines.slice(-Math.max(0, limit));
    const parsed = [];
    for (const line of tail) {
      try { parsed.push(JSON.parse(line)); } catch {}
    }
    return parsed;
  } catch {
    return [];
  }
}

function getStats() {
  return { ..._stats, bufferDepth: _buffer.length, disabledReason: _disabledReason };
}

/**
 * Test/admin hook: drain + close the stream. Mostly for tests; production
 * callers can call this on graceful shutdown.
 */
function shutdown() {
  if (_flushTimer) { clearTimeout(_flushTimer); _flushTimer = null; }
  _flush();
}

module.exports = {
  logEvent,
  info,
  warn,
  error,
  readRecent,
  getStats,
  shutdown,
  // Exported for tests
  _logPath,
  ROTATION_SIZE_BYTES,
  FLUSH_BATCH_SIZE,
};
