/**
 * dispatch-persister.mjs — persist the last dispatch for each world.
 *
 * The world watchdog's recovery hook reads this to replay the last
 * unanswered prompt when it auto-recovers a wedged claude process.
 *
 * Contract:
 *   persist({ worldId, messageId, prompt, source, statePath?, now? })
 *     Atomically writes ~/.olam/worlds/<worldId>/state/last-dispatch.json.
 *     Overwrites any previous file — only the LATEST dispatch matters for
 *     replay. Atomic write (tmp + fs.rename) prevents partial-write residue
 *     from corrupting recovery reads.
 *
 *   read({ worldId, statePath? })
 *     Returns { messageId, prompt, dispatchedAt, source } or null.
 *     null on ENOENT (no dispatch persisted yet) — never throws.
 *     null on JSON parse error (logs + skips) — never throws on corrupt file.
 *
 * Multiple worlds are independent: world A and world B have separate files.
 * Multiple concurrent persist() calls for the SAME world are safe — each
 * write is a rename of a tmp file so the worst case is one write winning.
 *
 * @see docs/architecture/world-watchdog.md
 */

import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';

// Default base path under which per-world state directories live.
const DEFAULT_STATE_BASE = path.join(os.homedir(), '.olam', 'worlds');

/**
 * Derive the path to last-dispatch.json for a world.
 *
 * @param {string} worldId
 * @param {string} [stateBase]  Override the base directory (for tests).
 * @returns {string}
 */
export function lastDispatchPath(worldId, stateBase = DEFAULT_STATE_BASE) {
  return path.join(stateBase, worldId, 'state', 'last-dispatch.json');
}

/**
 * Persist the last dispatch for a world.
 *
 * @param {{
 *   worldId: string,
 *   messageId: string,
 *   prompt: string,
 *   source: string,
 *   statePath?: string,
 *   now?: () => number,
 * }} opts
 * @returns {Promise<void>}
 */
export async function persist({
  worldId,
  messageId,
  prompt,
  source,
  statePath,
  now = () => Date.now(),
}) {
  const filePath = statePath ?? lastDispatchPath(worldId);
  const dir = path.dirname(filePath);
  const tmpPath = `${filePath}.tmp`;

  const record = {
    messageId,
    prompt,
    dispatchedAt: new Date(now()).toISOString(),
    source,
  };

  // Ensure the directory exists.
  await fs.mkdir(dir, { recursive: true });

  // Atomic write: write to .tmp then rename over the target.
  await fs.writeFile(tmpPath, JSON.stringify(record, null, 2) + '\n', 'utf8');
  await fs.rename(tmpPath, filePath);
}

/**
 * Fire-and-forget persist wrapper used at the dispatch call-sites.
 *
 * Centralises the void/.catch boilerplate so the two enrichment sites
 * (pr-nanny + /api/cloud-dispatch) can't drift on future changes.
 * Logs failures via the supplied logSource tag; never throws.
 *
 * @param {{
 *   worldId: string,
 *   messageId: string,
 *   prompt: string,
 *   source: string,
 *   logSource?: string,
 *   statePath?: string,
 *   now?: () => number,
 * }} opts
 * @returns {void}
 */
export function safePersistLastDispatch(opts) {
  const { logSource = opts.source, ...persistOpts } = opts;
  void persist(persistOpts).catch((err) => {
    console.warn(
      `[${logSource}] persistLastDispatch failed (non-fatal): ${err?.message ?? err}`,
    );
  });
}

/**
 * Read the last persisted dispatch for a world.
 *
 * @param {{
 *   worldId: string,
 *   statePath?: string,
 * }} opts
 * @returns {Promise<{ messageId: string, prompt: string, dispatchedAt: string, source: string } | null>}
 */
export async function read({ worldId, statePath }) {
  const filePath = statePath ?? lastDispatchPath(worldId);

  let raw;
  try {
    raw = await fs.readFile(filePath, 'utf8');
  } catch (err) {
    if (err?.code === 'ENOENT') return null;
    // Other I/O errors (e.g. permissions) — log + return null (fail-soft).
    console.error(`[dispatch-persister] readFile ${filePath}: ${err?.message ?? err}`);
    return null;
  }

  try {
    const parsed = JSON.parse(raw);
    // Basic shape validation — don't throw on corrupt file.
    if (
      typeof parsed !== 'object' ||
      parsed === null ||
      typeof parsed.messageId !== 'string' ||
      typeof parsed.prompt !== 'string' ||
      typeof parsed.dispatchedAt !== 'string' ||
      typeof parsed.source !== 'string'
    ) {
      console.error(`[dispatch-persister] ${filePath}: unexpected shape, skipping`);
      return null;
    }
    return {
      messageId: parsed.messageId,
      prompt: parsed.prompt,
      dispatchedAt: parsed.dispatchedAt,
      source: parsed.source,
    };
  } catch (err) {
    console.error(`[dispatch-persister] ${filePath}: JSON parse error: ${err?.message ?? err}`);
    return null;
  }
}
