// wall-e/tools/event-log.js
// Inspired by OpenCode's SyncEvent system and event sourcing pattern
// (https://github.com/anomalyco/opencode, packages/opencode/src/sync/index.ts,
//  packages/opencode/src/sync/event.sql.ts)
// Append-only event log for coding sessions, backed by SQLite.
'use strict';

const crypto = require('node:crypto');

/**
 * EventLog — append-only event store for coding sessions.
 *
 * OpenCode uses an event sourcing pattern with projectors and sequence IDs.
 * Wall-E simplifies this to a straightforward append + replay model:
 * - Events are appended with auto-incrementing IDs
 * - Replay returns events in order
 * - No projectors needed — consumers read events directly
 *
 * DB table: coding_events
 *   id          TEXT PRIMARY KEY
 *   session_id  TEXT NOT NULL
 *   event_type  TEXT NOT NULL
 *   payload     TEXT (JSON)
 *   timestamp   TEXT NOT NULL (ISO 8601)
 *
 * Migration is lazy — table is created on first use via ensureTable().
 */
class EventLog {
  /**
   * @param {import('better-sqlite3').Database} db - SQLite database connection
   */
  constructor(db) {
    if (!db) throw new Error('EventLog requires a database connection');
    this._db = db;
    this._tableReady = false;
  }

  /**
   * Ensure the coding_events table exists.
   * Called lazily on first operation. Safe to call multiple times.
   */
  ensureTable() {
    if (this._tableReady) return;
    this._db.exec(`
      CREATE TABLE IF NOT EXISTS coding_events (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        event_type TEXT NOT NULL,
        payload TEXT DEFAULT '{}',
        timestamp TEXT NOT NULL
      )
    `);
    // Index for efficient replay by session
    this._db.exec(`
      CREATE INDEX IF NOT EXISTS idx_coding_events_session
        ON coding_events (session_id, timestamp ASC)
    `);
    // Index for event type queries
    this._db.exec(`
      CREATE INDEX IF NOT EXISTS idx_coding_events_type
        ON coding_events (event_type)
    `);
    this._tableReady = true;
  }

  /**
   * Append an event to the log.
   *
   * @param {string} sessionId - Coding session identifier
   * @param {string} eventType - Event type (e.g., 'task.start', 'subtask.end', 'tool.call')
   * @param {object} [payload={}] - Event payload (serialized as JSON)
   * @param {string} [timestamp] - ISO timestamp (defaults to now)
   * @returns {{ id: string, sessionId: string, eventType: string, timestamp: string }}
   */
  append(sessionId, eventType, payload = {}, timestamp) {
    if (!sessionId) throw new Error('sessionId is required');
    if (!eventType) throw new Error('eventType is required');
    this.ensureTable();

    const id = crypto.randomUUID();
    const ts = timestamp || new Date().toISOString();
    const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload);

    this._db.prepare(`
      INSERT INTO coding_events (id, session_id, event_type, payload, timestamp)
      VALUES (?, ?, ?, ?, ?)
    `).run(id, sessionId, eventType, payloadStr, ts);

    return { id, sessionId, eventType, timestamp: ts };
  }

  /**
   * Replay all events for a session, in chronological order.
   *
   * @param {string} sessionId
   * @returns {Array<{ id, sessionId, eventType, payload, timestamp }>}
   */
  replay(sessionId) {
    if (!sessionId) throw new Error('sessionId is required');
    this.ensureTable();

    const rows = this._db.prepare(
      'SELECT id, session_id, event_type, payload, timestamp FROM coding_events WHERE session_id = ? ORDER BY timestamp ASC, rowid ASC'
    ).all(sessionId);

    return rows.map(_deserializeRow);
  }

  /**
   * Replay events for a session after a given timestamp (incremental replay).
   * Useful for catching up on events since a known checkpoint.
   *
   * @param {string} sessionId
   * @param {string} afterTimestamp - ISO timestamp; only events strictly after this are returned
   * @returns {Array<{ id, sessionId, eventType, payload, timestamp }>}
   */
  replaySince(sessionId, afterTimestamp) {
    if (!sessionId) throw new Error('sessionId is required');
    if (!afterTimestamp) throw new Error('afterTimestamp is required');
    this.ensureTable();

    const rows = this._db.prepare(
      'SELECT id, session_id, event_type, payload, timestamp FROM coding_events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp ASC, rowid ASC'
    ).all(sessionId, afterTimestamp);

    return rows.map(_deserializeRow);
  }

  /**
   * Get the count of events for a session.
   * @param {string} sessionId
   * @returns {number}
   */
  count(sessionId) {
    this.ensureTable();
    if (sessionId) {
      const row = this._db.prepare(
        'SELECT COUNT(*) as cnt FROM coding_events WHERE session_id = ?'
      ).get(sessionId);
      return row.cnt;
    }
    const row = this._db.prepare('SELECT COUNT(*) as cnt FROM coding_events').get();
    return row.cnt;
  }

  /**
   * List distinct session IDs that have events.
   * @returns {string[]}
   */
  listSessions() {
    this.ensureTable();
    return this._db.prepare(
      'SELECT session_id FROM coding_events GROUP BY session_id ORDER BY MIN(rowid)'
    ).all().map(r => r.session_id);
  }

  /**
   * Delete all events for a session.
   * @param {string} sessionId
   * @returns {number} Number of deleted events
   */
  deleteSession(sessionId) {
    if (!sessionId) throw new Error('sessionId is required');
    this.ensureTable();
    const result = this._db.prepare(
      'DELETE FROM coding_events WHERE session_id = ?'
    ).run(sessionId);
    return result.changes;
  }
}

// ── Standard event types for coding sessions ──
// Modeled after OpenCode's sync event types
const EventTypes = {
  TASK_START: 'task.start',
  TASK_COMPLETE: 'task.complete',
  TASK_ERROR: 'task.error',
  SUBTASK_START: 'subtask.start',
  SUBTASK_END: 'subtask.end',
  TOOL_CALL: 'tool.call',
  TOOL_RESULT: 'tool.result',
  LLM_REQUEST: 'llm.request',
  LLM_RESPONSE: 'llm.response',
  FILE_EDITED: 'file.edited',
  FILE_CREATED: 'file.created',
  ERROR: 'error',
};

// ── Helper ──

function _deserializeRow(row) {
  let payload = {};
  try {
    payload = row.payload ? JSON.parse(row.payload) : {};
  } catch {
    payload = { _raw: row.payload };
  }
  return {
    id: row.id,
    sessionId: row.session_id,
    eventType: row.event_type,
    payload,
    timestamp: row.timestamp,
  };
}

module.exports = { EventLog, EventTypes };
