// log-rotate.mjs — Bounded-size log rotation for the cron-runner.
// Zero dependencies. Standard Linux logrotate idiom: when logPath exceeds
// maxSizeBytes, shift generations N..1 down by one, drop the oldest, and
// move logPath → logPath.1. The next caller writes to a fresh logPath.

import { existsSync, statSync, renameSync, unlinkSync } from 'node:fs';

const DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
const DEFAULT_MAX_GENERATIONS = 5;

function silentRename(from, to) {
  try {
    renameSync(from, to);
    return true;
  } catch {
    return false;
  }
}

function silentUnlink(path) {
  try {
    unlinkSync(path);
    return true;
  } catch {
    return false;
  }
}

/**
 * Rotate logPath if it exceeds maxSizeBytes. Keeps at most maxGenerations
 * total files (1 active + maxGenerations-1 rotated copies). No-op if logPath
 * is missing or under threshold.
 *
 * @param {string} logPath
 * @param {object} [opts]
 * @param {number} [opts.maxSizeBytes] — default 10 MB
 * @param {number} [opts.maxGenerations] — default 5; total active+rotated
 * @returns {{rotated: boolean, dropped: string|null, kept: string[]}}
 */
export function rotateLog(logPath, opts = {}) {
  const result = { rotated: false, dropped: null, kept: [] };
  if (!logPath || typeof logPath !== 'string') {
    throw new Error('rotateLog: logPath is required');
  }
  const maxSizeBytes = opts.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
  const maxGenerations = Math.max(1, opts.maxGenerations ?? DEFAULT_MAX_GENERATIONS);

  if (!existsSync(logPath)) return result;

  let size;
  try {
    size = statSync(logPath).size;
  } catch {
    return result;
  }
  if (size <= maxSizeBytes) return result;

  // The highest rotated generation we keep is maxGenerations - 1.
  // (Active log = generation 0; rotated copies = generations 1..N-1.)
  const lastGen = maxGenerations - 1;

  if (lastGen === 0) {
    // Only the active log is kept — rotation = drop the previous active.
    silentUnlink(logPath);
    result.rotated = true;
    result.dropped = logPath;
    return result;
  }

  // Drop the oldest generation if it exists.
  const oldest = `${logPath}.${lastGen}`;
  if (existsSync(oldest)) {
    silentUnlink(oldest);
    result.dropped = oldest;
  }

  // Shift down: .N-1 → .N, .N-2 → .N-1, ..., .1 → .2.
  for (let i = lastGen - 1; i >= 1; i--) {
    const from = `${logPath}.${i}`;
    const to = `${logPath}.${i + 1}`;
    if (existsSync(from)) {
      silentRename(from, to);
      result.kept.push(to);
    }
  }

  // Active log → .1.
  if (silentRename(logPath, `${logPath}.1`)) {
    result.kept.unshift(`${logPath}.1`);
  }

  result.rotated = true;
  return result;
}
