// RecoveryLedger — append-only NDJSON persistence for recovery attempts.
//
// Each attempt writes one JSON line to the ledger file. The file grows
// monotonically; entries are never updated in-place. This keeps the
// ledger auditable and safe to tail/parse with `jq` while the process
// is running.
//
// Default path: ~/.olam/logs/recovery-ledger.ndjson
// Override: set OLAM_RECOVERY_LEDGER_PATH (useful in tests — point at a
// tmp file to isolate test runs from the real operator ledger).

import { open, mkdir, access } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
import { redactSensitive } from '../observability/redactor.mjs';

export const DEFAULT_LEDGER_PATH =
  process.env.OLAM_RECOVERY_LEDGER_PATH ??
  join(homedir(), '.olam', 'logs', 'recovery-ledger.ndjson');

/**
 * @typedef {object} RecoveryLedgerEntry
 * @property {string}                   worldId
 * @property {string | null}            failureKind    — WorldStartupFailureKind or null (non-FSM trigger)
 * @property {string}                   scenario       — kebab-case scenario name, or 'unmatched'
 * @property {import('./recipes.mjs').RecoveryStep[]} stepsRun  — steps actually executed (may be partial on failure)
 * @property {number}                   startedAt      — epoch ms
 * @property {number}                   endedAt        — epoch ms
 * @property {'success' | 'failed' | 'escalated'} outcome
 * @property {string}                   [errorReason]  — set on failed/escalated outcomes
 */

/**
 * Append a single RecoveryLedgerEntry to the ledger file.
 *
 * @param {RecoveryLedgerEntry} entry
 * @param {string} [ledgerPath]
 * @returns {Promise<void>}
 */
export async function appendLedgerEntry(entry, ledgerPath = DEFAULT_LEDGER_PATH) {
  await mkdir(dirname(ledgerPath), { recursive: true });
  const fh = await open(ledgerPath, 'a');
  try {
    await fh.write(JSON.stringify(redactSensitive(entry)) + '\n');
  } finally {
    await fh.close();
  }
}

/**
 * Read all entries from the ledger (in append order).
 *
 * @param {string} [ledgerPath]
 * @returns {Promise<RecoveryLedgerEntry[]>}
 */
export async function readAllLedgerEntries(ledgerPath = DEFAULT_LEDGER_PATH) {
  /** @type {RecoveryLedgerEntry[]} */
  const entries = [];

  // Check existence before streaming — createReadStream emits ENOENT as an
  // error event (not a synchronous throw), which propagates through the
  // `for await` loop and would reject the caller. An explicit access check
  // keeps the "not yet written" path simple.
  try {
    await access(ledgerPath);
  } catch {
    return entries; // File does not exist yet.
  }

  const stream = createReadStream(ledgerPath, { encoding: 'utf8' });
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
  for await (const line of rl) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    try {
      entries.push(JSON.parse(trimmed));
    } catch {
      // Malformed line — skip and continue.
    }
  }
  return entries;
}

/**
 * Find the most recent ledger entry for a (worldId, failureKind) pair.
 * Returns undefined if no prior entry exists.
 *
 * @param {string}      worldId
 * @param {string|null} failureKind
 * @param {string}      [ledgerPath]
 * @returns {Promise<RecoveryLedgerEntry | undefined>}
 */
export async function findPriorEntry(worldId, failureKind, ledgerPath = DEFAULT_LEDGER_PATH) {
  const all = await readAllLedgerEntries(ledgerPath);
  // Walk in reverse to find the most recent match.
  for (let i = all.length - 1; i >= 0; i--) {
    const e = all[i];
    if (e.worldId === worldId && e.failureKind === (failureKind ?? null)) {
      return e;
    }
  }
  return undefined;
}
