import { uuidv7 } from 'uuidv7';
const POLL_INTERVAL_MS = 1000;
const MAX_RETRY = 3;
const BATCH_SIZE = 10;
const BACKOFF_MS = [1000, 5000, 10000];
export class EventOutbox {
    db;
    eventBus;
    enqueueStmt;
    markProcessingStmt;
    markPublishedStmt;
    markPendingRetryStmt;
    markFailedStmt;
    moveToDeadLetterStmt;
    intervalId = null;
    processing = false;
    constructor(storage, eventBus) {
        this.db = storage.getDatabase();
        this.eventBus = eventBus;
        this.initTables();
        this.enqueueStmt = this.db.prepare(`
      INSERT INTO event_outbox (id, eventType, payload, status, retryCount, createdAt)
      VALUES (?, ?, ?, 'pending', 0, ?)
    `);
        this.markProcessingStmt = this.db.prepare(`
      UPDATE event_outbox SET status = 'processing' WHERE id = ? AND status = 'pending'
    `);
        this.markPublishedStmt = this.db.prepare(`
      UPDATE event_outbox SET status = 'published', publishedAt = ? WHERE id = ?
    `);
        this.markPendingRetryStmt = this.db.prepare(`
      UPDATE event_outbox SET status = 'pending', retryCount = retryCount + 1, lastRetryAt = ? WHERE id = ?
    `);
        this.markFailedStmt = this.db.prepare(`
      UPDATE event_outbox SET status = 'failed' WHERE id = ?
    `);
        this.moveToDeadLetterStmt = this.db.prepare(`
      INSERT INTO dead_letter_events (id, originalEventId, eventType, payload, error, retryCount, failedAt)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `);
    }
    initTables() {
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS event_outbox (
        id TEXT PRIMARY KEY,
        eventType TEXT NOT NULL,
        payload TEXT NOT NULL,
        status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'published', 'failed')),
        retryCount INTEGER DEFAULT 0,
        lastRetryAt TEXT,
        createdAt TEXT NOT NULL,
        publishedAt TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_outbox_status ON event_outbox(status, createdAt);

      CREATE TABLE IF NOT EXISTS dead_letter_events (
        id TEXT PRIMARY KEY,
        originalEventId TEXT NOT NULL,
        eventType TEXT NOT NULL,
        payload TEXT NOT NULL,
        error TEXT NOT NULL,
        retryCount INTEGER NOT NULL,
        failedAt TEXT NOT NULL,
        resolvedAt TEXT,
        status TEXT DEFAULT 'failed' CHECK(status IN ('failed', 'retried', 'discarded'))
      );
    `);
    }
    enqueue(event) {
        const id = uuidv7();
        this.enqueueStmt.run(id, event.eventType, JSON.stringify(event.payload), new Date().toISOString());
        return id;
    }
    start() {
        this.recoverProcessing();
        this.intervalId = setInterval(() => {
            if (!this.processing) {
                this.processOutbox();
            }
        }, POLL_INTERVAL_MS);
    }
    stop() {
        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
    }
    processOutbox() {
        this.processing = true;
        try {
            const pending = this.db
                .prepare(`SELECT * FROM event_outbox WHERE status = 'pending' ORDER BY createdAt ASC LIMIT ?`)
                .all(BATCH_SIZE);
            for (const row of pending) {
                if (row.retryCount > 0 && row.lastRetryAt) {
                    const backoff = BACKOFF_MS[Math.min(row.retryCount - 1, BACKOFF_MS.length - 1)];
                    const elapsed = Date.now() - new Date(row.lastRetryAt).getTime();
                    if (elapsed < backoff)
                        continue;
                }
                this.processOne(row);
            }
        }
        finally {
            this.processing = false;
        }
    }
    getPendingCount() {
        return this.db
            .prepare(`SELECT COUNT(*) as count FROM event_outbox WHERE status = 'pending'`)
            .get().count;
    }
    getDeadLetterCount() {
        return this.db
            .prepare(`SELECT COUNT(*) as count FROM dead_letter_events WHERE status = 'failed'`)
            .get().count;
    }
    processOne(row) {
        const changed = this.markProcessingStmt.run(row.id);
        if (changed.changes === 0)
            return;
        try {
            const payload = JSON.parse(row.payload);
            this.eventBus.emitIdempotent(row.id, row.eventType, payload);
            const syncErrors = this.eventBus.getLastSyncErrors();
            if (syncErrors.length > 0) {
                throw syncErrors[0];
            }
            this.markPublishedStmt.run(new Date().toISOString(), row.id);
        }
        catch (err) {
            const now = new Date().toISOString();
            const newRetryCount = row.retryCount + 1;
            if (newRetryCount >= MAX_RETRY) {
                this.db.transaction(() => {
                    this.markFailedStmt.run(row.id);
                    this.moveToDeadLetterStmt.run(uuidv7(), row.id, row.eventType, row.payload, err instanceof Error ? err.message : String(err), newRetryCount, now);
                })();
                process.stderr.write(`[EventOutbox] Event ${row.id} moved to dead letter after ${newRetryCount} retries\n`);
            }
            else {
                this.markPendingRetryStmt.run(now, row.id);
            }
        }
    }
    recoverProcessing() {
        this.db
            .prepare(`UPDATE event_outbox SET status = 'pending' WHERE status = 'processing'`)
            .run();
    }
}
//# sourceMappingURL=EventOutbox.js.map