import { o as createSubsystemLogger } from "./entry.js";
import { t as applySqlitePragmas } from "./sqlite-pragmas-DDlIB_V6.js";
import { t as requireNodeSqlite } from "./sqlite-CVrhPija.js";
import path from "node:path";
import { readFileSync, statSync, truncateSync } from "node:fs";

//#region src/events/crash-journal.ts
/**
* Crash journal — synchronous file-based crash capture for cross-restart replay.
*
* writeCrashJournal() appends crash entries during uncaughtException/unhandledRejection.
* replayCrashJournal() reads and drains the journal on next startup.
*
* Moved from gateway/crash-capture.ts into the events module.
*
* @module
*/
const log$2 = createSubsystemLogger("crash-journal");
const MAX_JOURNAL_BYTES = 1024 * 1024;
function getJournalPath(stateDir) {
	return path.join(stateDir, "crash-journal.jsonl");
}
/**
* Read and drain the crash journal, returning all entries.
* Truncates the journal file after reading.
*/
function replayCrashJournal(stateDir) {
	const journalPath = getJournalPath(stateDir);
	const entries = [];
	try {
		const lines = readFileSync(journalPath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
		for (const line of lines) try {
			entries.push(JSON.parse(line));
		} catch {
			log$2.warn("skipping malformed crash journal entry");
		}
		truncateSync(journalPath, 0);
		if (entries.length > 0) log$2.info(`replayed ${entries.length} crash journal entries`);
	} catch (err) {
		if (err instanceof Error && "code" in err && err.code !== "ENOENT") log$2.warn(`failed to read crash journal: ${String(err)}`);
	}
	return entries;
}

//#endregion
//#region src/events/store.ts
/**
* Local SQLite event store — gateway-native event persistence.
*
* Uses node:sqlite (DatabaseSync) with WAL mode for crash safety and
* concurrent read support. This is the canonical data layer: all events
* are written here first, then synced to Turso write-behind.
*
* @module
*/
const INIT_SQL = `
CREATE TABLE IF NOT EXISTS events (
  id             INTEGER PRIMARY KEY AUTOINCREMENT,
  category       TEXT    NOT NULL,
  severity       TEXT    NOT NULL,
  event          TEXT    NOT NULL,
  message        TEXT    NOT NULL,
  agent_id       TEXT,
  correlation_id TEXT,
  metadata       TEXT,
  timestamp      INTEGER NOT NULL,
  created_at     INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_events_category_ts   ON events(category, timestamp);
CREATE INDEX IF NOT EXISTS idx_events_agent_ts      ON events(agent_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_events_correlation    ON events(correlation_id);
CREATE INDEX IF NOT EXISTS idx_events_ts             ON events(timestamp);
`;
function rowToRecord(row) {
	return {
		id: row.id,
		category: row.category,
		severity: row.severity,
		event: row.event,
		message: row.message,
		agentId: row.agent_id ?? void 0,
		correlationId: row.correlation_id ?? void 0,
		metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
		timestamp: row.timestamp,
		createdAt: row.created_at
	};
}
function computeBucketMs(since, until) {
	const range = until - since;
	if (range < 36e5) return 6e4;
	if (range < 864e5) return 9e5;
	if (range < 6048e5) return 36e5;
	return 864e5;
}
function createEventStore(config) {
	const { DatabaseSync } = requireNodeSqlite();
	const db = new DatabaseSync(config.localDbPath);
	applySqlitePragmas(db);
	db.exec(INIT_SQL);
	const insertStmt = db.prepare(`
    INSERT INTO events (category, severity, event, message, agent_id, correlation_id, metadata, timestamp, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
	const getStmt = db.prepare(`SELECT * FROM events WHERE id = ?`);
	const unsyncedStmt = db.prepare(`SELECT * FROM events WHERE id > ? ORDER BY id ASC LIMIT ?`);
	return {
		insert(input) {
			const meta = input.metadata ? JSON.stringify(input.metadata) : null;
			const result = insertStmt.run(input.category, input.severity, input.event, input.message, input.agentId ?? null, input.correlationId ?? null, meta, input.timestamp, Date.now());
			return Number(result.lastInsertRowid);
		},
		query(opts) {
			const conditions = [];
			const params = [];
			if (opts.category) {
				conditions.push("category = ?");
				params.push(opts.category);
			}
			if (opts.severity) {
				conditions.push("severity = ?");
				params.push(opts.severity);
			}
			if (opts.agentId) {
				conditions.push("agent_id = ?");
				params.push(opts.agentId);
			}
			if (opts.correlationId) {
				conditions.push("correlation_id = ?");
				params.push(opts.correlationId);
			}
			if (opts.since != null) {
				conditions.push("timestamp >= ?");
				params.push(opts.since);
			}
			if (opts.until != null) {
				conditions.push("timestamp <= ?");
				params.push(opts.until);
			}
			if (opts.eventPattern) {
				conditions.push("event LIKE ?");
				params.push(opts.eventPattern);
			}
			if (opts.search) {
				conditions.push("message LIKE ?");
				params.push(opts.search);
			}
			const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
			const limit = opts.limit ?? 1e4;
			const offset = opts.offset ?? 0;
			const sql = `SELECT * FROM events ${where} ORDER BY id DESC LIMIT ? OFFSET ?`;
			params.push(limit, offset);
			return db.prepare(sql).all(...params).map(rowToRecord);
		},
		get(id) {
			const row = getStmt.get(id);
			return row ? rowToRecord(row) : null;
		},
		summary(opts) {
			const sinceClause = opts?.since != null ? "WHERE timestamp >= ?" : "";
			const params = opts?.since != null ? [opts.since] : [];
			const totalRow = db.prepare(`SELECT COUNT(*) as cnt FROM events ${sinceClause}`).get(...params);
			const catRows = db.prepare(`SELECT category, COUNT(*) as cnt FROM events ${sinceClause} GROUP BY category`).all(...params);
			const sevRows = db.prepare(`SELECT severity, COUNT(*) as cnt FROM events ${sinceClause} GROUP BY severity`).all(...params);
			const byCategory = {};
			for (const r of catRows) byCategory[r.category] = r.cnt;
			const bySeverity = {};
			for (const r of sevRows) bySeverity[r.severity] = r.cnt;
			return {
				total: totalRow.cnt,
				byCategory,
				bySeverity
			};
		},
		timeline(opts) {
			const bucketMs = computeBucketMs(opts.since, opts.until);
			return db.prepare(`SELECT (timestamp / ? ) * ? as bucket, COUNT(*) as count
           FROM events
           WHERE timestamp >= ? AND timestamp <= ?
           GROUP BY bucket
           ORDER BY bucket`).all(bucketMs, bucketMs, opts.since, opts.until).map((r) => ({
				bucket: r.bucket,
				count: r.count
			}));
		},
		cleanup(retention) {
			const now = Date.now();
			let totalDeleted = 0;
			const cats = db.prepare("SELECT DISTINCT category FROM events").all();
			const deleteStmt = db.prepare("DELETE FROM events WHERE category = ? AND timestamp < ?");
			for (const { category } of cats) {
				const cutoff = now - (retention[category] ?? retention._default ?? 30) * 24 * 36e5;
				const result = deleteStmt.run(category, cutoff);
				totalDeleted += Number(result.changes);
			}
			return totalDeleted;
		},
		getUnsyncedEvents(sinceId, batchSize) {
			return unsyncedStmt.all(sinceId, batchSize).map(rowToRecord);
		},
		dbSizeBytes() {
			if (config.localDbPath === ":memory:") return 0;
			try {
				return statSync(config.localDbPath).size;
			} catch {
				return 0;
			}
		},
		close() {
			db.close();
		},
		_db() {
			return db;
		}
	};
}

//#endregion
//#region src/events/turso-sync.ts
/**
* Turso write-behind sync engine — batches local events to remote Turso DB.
*
* Reads unsyncedEvents from the local EventStore and upserts them into
* the hub's `unified_events` table via `INSERT OR IGNORE` (dedup on
* serverId + localEventId). Uses exponential backoff on failures
* (max 30 minutes).
*
* Pattern follows the Turso client init approach used by the gateway.
*
* @module
*/
const log$1 = createSubsystemLogger("turso-sync");
const MAX_BACKOFF_MS = 1800 * 1e3;
/**
* Create a Turso sync engine. Returns null if Turso init fails
* (e.g. serverId not in servers table, network error).
*/
async function createTursoSync(opts) {
	const { store, tursoUrl, tursoAuthToken, serverId, syncIntervalMs, syncBatchSize } = opts;
	let client;
	let tenantId;
	try {
		const { createClient } = await import("@libsql/client");
		client = createClient({
			url: tursoUrl,
			authToken: tursoAuthToken
		});
		const result = await client.execute({
			sql: "SELECT tenant_id FROM servers WHERE id = ?",
			args: [serverId]
		});
		if (result.rows.length === 0) {
			log$1.warn("serverId not found in Turso servers table — sync disabled");
			client.close();
			return null;
		}
		const tid = result.rows[0].tenant_id;
		if (!tid) {
			log$1.warn("tenantId is null for serverId — sync disabled");
			client.close();
			return null;
		}
		tenantId = tid;
		log$1.info("turso sync initialized", {
			tursoUrl,
			serverId,
			tenantId
		});
	} catch (err) {
		log$1.warn("failed to initialize Turso sync client", { err: String(err) });
		return null;
	}
	let lastSyncedId = 0;
	let consecutiveFailures = 0;
	let currentBackoff = syncIntervalMs;
	let syncInProgress = false;
	let pendingHeartbeat = null;
	async function syncCycle() {
		if (syncInProgress) return;
		syncInProgress = true;
		try {
			const events = store.getUnsyncedEvents(lastSyncedId, syncBatchSize);
			const heartbeat = pendingHeartbeat;
			pendingHeartbeat = null;
			if (events.length === 0 && !heartbeat) return;
			const stmts = events.map((e) => ({
				sql: `INSERT OR IGNORE INTO unified_events (tenant_id, server_id, local_event_id, category, severity, event, message, agent_id, correlation_id, metadata, occurred_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
				args: [
					tenantId,
					serverId,
					e.id,
					e.category,
					e.severity,
					e.event,
					e.message,
					e.agentId ?? null,
					e.correlationId ?? null,
					e.metadata ? JSON.stringify(e.metadata) : null,
					e.timestamp,
					e.createdAt
				]
			}));
			if (heartbeat) stmts.push({
				sql: `INSERT INTO gateway_heartbeats (tenant_id, server_id, uptime_ms, active_sessions, active_agents, memory_rss_mb, credential_summary_json, channel_status_json, captured_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
				args: [
					tenantId,
					serverId,
					heartbeat.uptimeMs,
					heartbeat.activeSessions,
					heartbeat.activeAgents,
					heartbeat.memoryRssMb,
					heartbeat.credentialSummaryJson ?? null,
					heartbeat.channelStatusJson ?? null,
					heartbeat.capturedAt
				]
			});
			await client.batch(stmts);
			if (events.length > 0) lastSyncedId = Math.max(...events.map((e) => e.id));
			consecutiveFailures = 0;
			currentBackoff = syncIntervalMs;
			log$1.debug("sync cycle ok", {
				synced: events.length,
				lastSyncedId,
				heartbeat: !!heartbeat
			});
		} catch (err) {
			consecutiveFailures++;
			currentBackoff = Math.min(syncIntervalMs * Math.pow(2, consecutiveFailures), MAX_BACKOFF_MS);
			log$1.warn("sync cycle failed", {
				err: String(err),
				consecutiveFailures,
				nextRetryMs: currentBackoff
			});
		} finally {
			syncInProgress = false;
		}
	}
	const interval = setInterval(() => {
		syncCycle();
	}, syncIntervalMs);
	interval.unref?.();
	return {
		async flush() {
			await syncCycle();
		},
		stop() {
			clearInterval(interval);
			try {
				client.close();
			} catch {}
			log$1.info("turso sync stopped");
		},
		pushHeartbeat(heartbeat) {
			pendingHeartbeat = heartbeat;
		},
		get currentBackoffMs() {
			return currentBackoff;
		}
	};
}

//#endregion
//#region src/events/emitter.ts
/**
* Centralized event emitter — single emitEvent() entry point.
*
* All gateway event producers call emitEvent(). Internally:
* 1. Writes to local SQLite (synchronous via node:sqlite DatabaseSync)
* 2. Broadcasts "events.new" to connected WebSocket clients
* 3. Rate-limits high-frequency categories (tool, message)
*
* initEventSystem() is the full lifecycle wrapper called by server.impl.ts:
* - Creates EventStore
* - Replays crash journal into event store
* - Starts retention cleanup (immediate + daily)
* - Monitors DB size
* - Optionally starts TursoSyncEngine
* - Returns async shutdown that flushes sync before closing
*
* Singleton pattern matching existing reliability.ts style.
*
* @module
*/
const log = createSubsystemLogger("events");
/** Categories subject to rate limiting. */
const RATE_LIMITED_CATEGORIES = new Set(["tool", "message"]);
/** Rate limit window per event name (ms). */
const RATE_LIMIT_MS = 5e3;
/** Stale key cleanup interval (ms). */
const STALE_KEY_CLEANUP_MS = 5 * 6e4;
const rateLimitMap = /* @__PURE__ */ new Map();
let cleanupInterval = null;
function isRateLimited(category, eventName) {
	if (!RATE_LIMITED_CATEGORIES.has(category)) return false;
	const key = `${category}:${eventName}`;
	const now = Date.now();
	const last = rateLimitMap.get(key);
	if (last && now - last < RATE_LIMIT_MS) return true;
	rateLimitMap.set(key, now);
	return false;
}
function cleanupStaleKeys() {
	const now = Date.now();
	for (const [key, ts] of rateLimitMap) if (now - ts > RATE_LIMIT_MS * 2) rateLimitMap.delete(key);
}
let store = null;
let broadcastFn = null;
/**
* Initialize the event emitter singleton. Call once at gateway startup.
*
* @param config.storeConfig - EventStoreConfig for local SQLite
* @param config.broadcast - Optional broadcast function for WebSocket events
*/
function initEventEmitter(config) {
	if (store) shutdown();
	store = createEventStore(config.storeConfig);
	broadcastFn = config.broadcast ?? null;
	if (!cleanupInterval) {
		cleanupInterval = setInterval(cleanupStaleKeys, STALE_KEY_CLEANUP_MS);
		cleanupInterval.unref?.();
	}
	return {
		store,
		shutdown
	};
}
/**
* Emit an event. Safe to call from any context — never throws.
*
* Writes to local SQLite, broadcasts to WebSocket clients, and
* rate-limits high-frequency categories (tool, message).
*/
function emitEvent(input) {
	if (!store) {
		log.warn("emitEvent called before initEventEmitter — event dropped");
		return;
	}
	if (isRateLimited(input.category, input.event)) return;
	try {
		const id = store.insert(input);
		if (broadcastFn) {
			const record = store.get(id);
			if (record) broadcastFn("events.new", record, { dropIfSlow: true });
		}
	} catch (err) {
		log.debug(`emitEvent error: ${err instanceof Error ? err.message : String(err)}`);
	}
}
/**
* Get the event store instance, or null if not initialized.
*/
function getEventStore() {
	return store;
}
/**
* Shut down the event emitter. Closes store and clears state.
*/
function shutdown() {
	if (store) {
		try {
			store.close();
		} catch {}
		store = null;
	}
	broadcastFn = null;
	rateLimitMap.clear();
	if (cleanupInterval) {
		clearInterval(cleanupInterval);
		cleanupInterval = null;
	}
}
/** One day in milliseconds. */
const ONE_DAY_MS = 1440 * 60 * 1e3;
/**
* Initialize the full event system. Call once at gateway startup.
*
* - Creates local EventStore
* - Replays crash journal entries as "crash" events
* - Runs retention cleanup immediately and schedules daily
* - Checks DB size and emits warning if above threshold
* - Optionally starts TursoSyncEngine for write-behind
*
* Returns an async shutdown handle.
*/
async function initEventSystem(config) {
	const eventStore = initEventEmitter({
		storeConfig: config.storeConfig,
		broadcast: config.broadcast
	}).store;
	let tursoSync = null;
	let retentionInterval = null;
	if (config.stateDir) try {
		const entries = replayCrashJournal(config.stateDir);
		for (const entry of entries) eventStore.insert({
			category: "crash",
			severity: "critical",
			event: entry.event,
			message: entry.message,
			metadata: entry.metadata ? JSON.parse(entry.metadata) : void 0,
			timestamp: entry.timestamp
		});
		if (entries.length > 0) log.info(`replayed ${entries.length} crash journal entries into event store`);
	} catch (err) {
		log.warn(`crash journal replay failed: ${String(err)}`);
	}
	function runCleanup() {
		try {
			const deleted = eventStore.cleanup(config.storeConfig.retention);
			if (deleted > 0) log.info(`retention cleanup removed ${deleted} events`);
		} catch (err) {
			log.warn(`retention cleanup failed: ${String(err)}`);
		}
	}
	runCleanup();
	retentionInterval = setInterval(runCleanup, ONE_DAY_MS);
	retentionInterval.unref?.();
	function checkDbSize() {
		try {
			const sizeMb = eventStore.dbSizeBytes() / (1024 * 1024);
			if (sizeMb > config.storeConfig.sizeWarningMb) {
				emitEvent({
					category: "system",
					severity: "medium",
					event: "events.db_size_warning",
					message: `Event store DB size (${sizeMb.toFixed(1)} MB) exceeds threshold (${config.storeConfig.sizeWarningMb} MB)`,
					metadata: {
						sizeMb,
						thresholdMb: config.storeConfig.sizeWarningMb
					},
					timestamp: Date.now()
				});
				log.warn(`event store DB size warning: ${sizeMb.toFixed(1)} MB > ${config.storeConfig.sizeWarningMb} MB`);
			}
		} catch (err) {
			log.debug(`DB size check failed: ${String(err)}`);
		}
	}
	checkDbSize();
	if (config.tursoUrl && config.serverId) try {
		tursoSync = await createTursoSync({
			store: eventStore,
			tursoUrl: config.tursoUrl,
			tursoAuthToken: config.tursoAuthToken,
			serverId: config.serverId,
			syncIntervalMs: config.syncIntervalMs ?? 3e4,
			syncBatchSize: config.syncBatchSize ?? 500
		});
	} catch (err) {
		log.warn(`turso sync init failed: ${String(err)}`);
	}
	return {
		pushHeartbeat(heartbeat) {
			tursoSync?.pushHeartbeat(heartbeat);
		},
		async shutdown() {
			if (tursoSync) {
				try {
					await tursoSync.flush();
				} catch {}
				tursoSync.stop();
				tursoSync = null;
			}
			if (retentionInterval) {
				clearInterval(retentionInterval);
				retentionInterval = null;
			}
			shutdown();
		}
	};
}

//#endregion
export { getEventStore as n, initEventSystem as r, emitEvent as t };