// wall-e/tools/file-tracker.js
// Ported from OpenCode (https://github.com/anomalyco/opencode)
// Source: packages/opencode/src/file/time.ts
// Tracks mtime/size per session to detect stale edits.
'use strict';

const fs = require('node:fs');
const path = require('node:path');

// Map<sessionId, Map<filePath, { mtime: number, size: number }>>
const sessions = new Map();

/**
 * Record the current mtime and size of a file when it is read.
 * Called from readFile() in local-tools.js.
 * @param {string} filePath - Absolute path
 * @param {string} sessionId - Coding session ID
 */
function recordRead(filePath, sessionId) {
  recordCurrent(filePath, sessionId);
}

/**
 * Record the current mtime and size of a file after this session writes it.
 * This keeps stale-edit protection focused on external races, not on the
 * agent's own prior successful edits.
 * @param {string} filePath - Absolute path
 * @param {string} sessionId - Coding session ID
 */
function recordCurrent(filePath, sessionId) {
  if (!sessionId) return;
  const resolved = path.resolve(filePath);
  const stat = fs.statSync(resolved);
  if (!sessions.has(sessionId)) sessions.set(sessionId, new Map());
  sessions.get(sessionId).set(resolved, {
    mtime: stat.mtimeMs,
    size: stat.size,
  });
}

/**
 * Forget a tracked file after this session deletes it.
 * @param {string} filePath - Absolute path
 * @param {string} sessionId - Coding session ID
 */
function forgetFile(filePath, sessionId) {
  if (!sessionId) return;
  const resolved = path.resolve(filePath);
  const sessionMap = sessions.get(sessionId);
  if (sessionMap) sessionMap.delete(resolved);
}

/**
 * Assert a file has not changed since the last recordRead().
 * Called from editFile()/writeFile() in local-tools.js before writing.
 * Throws if the file was modified externally.
 * @param {string} filePath - Absolute path
 * @param {string} sessionId - Coding session ID
 */
function assertUnchanged(filePath, sessionId) {
  const resolved = path.resolve(filePath);
  const sessionMap = sessions.get(sessionId);
  if (!sessionMap) return; // no session records -- allow
  const recorded = sessionMap.get(resolved);
  if (!recorded) return; // no record for this file -- allow (new file)

  let current;
  try {
    current = fs.statSync(resolved);
  } catch (err) {
    if (err.code === 'ENOENT') {
      // File was deleted since read
      throw new Error(`File ${filePath} was deleted since last read. Re-read before editing.`);
    }
    throw err;
  }

  if (current.mtimeMs !== recorded.mtime || current.size !== recorded.size) {
    throw new Error(
      `File ${filePath} was modified since last read. Re-read before editing.\n` +
      `Recorded: mtime=${new Date(recorded.mtime).toISOString()}, size=${recorded.size}\n` +
      `Current:  mtime=${new Date(current.mtimeMs).toISOString()}, size=${current.size}`
    );
  }
}

/**
 * Clear all file tracking state for a session.
 * Called when a coding session ends.
 * @param {string} sessionId
 */
function clearSession(sessionId) {
  sessions.delete(sessionId);
}

module.exports = { recordRead, recordCurrent, forgetFile, assertUnchanged, clearSession };
