import { appendFileSync, existsSync, mkdirSync, promises, readFileSync, renameSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import { homedir } from "node:os";

//#region src/sessions/event-log.ts
/**
* Per-session append-only event log.
*
* Foundation for the new `session.*` RPC family. Replaces the implicit
* "chat history with role conflation + injected messages" model with an
* explicit, append-only stream where every event has a `participantId`,
* a typed `kind`, and a monotonic `eventId`.
*
* Architecture:
*   - One log per `sessionId`, identified by a free-form string.
*   - Each `appendEvent` allocates the next monotonic `eventId` for that
*     session and notifies all subscribers in order.
*   - Subscribers can resume from any prior eventId — the log keeps a
*     bounded in-memory tail (default 1000 events) plus the on-disk
*     JSONL mirror at `~/.skykoi/sessions/<sessionId>.events.jsonl` for
*     deeper replay.
*   - Cancel signals are first-class events; the koi runtime's main
*     loop watches for them and aborts via AbortSignal.
*
* Threading: this module is the single source of truth on the gateway
* for "what happened in session X". The platform-side Postgres mirror
* (Session/Participant/SessionEvent tables) is a durable shadow; the
* gateway is the realtime hot path.
*/
const SESSION_PROTOCOL_VERSION = 1;
const TAIL_LIMIT = 1e3;
const SESSIONS_ROOT = join(homedir(), ".skykoi", "sessions");
const states = /* @__PURE__ */ new Map();
function diskPathFor(sessionId) {
	return join(SESSIONS_ROOT, `${sessionId}.events.jsonl`);
}
function eventIdToBigInt(value) {
	try {
		return BigInt(value || "0");
	} catch {
		return 0n;
	}
}
function normalizeLimit(limit) {
	if (!Number.isFinite(limit)) return 200;
	return Math.min(Math.max(Math.floor(limit), 1), 1e4);
}
function normalizeMentions(mentions) {
	if (!Array.isArray(mentions)) return void 0;
	const out = Array.from(new Set(mentions.map((mention) => mention.trim()).filter(Boolean)));
	return out.length > 0 ? out : void 0;
}
function getOrCreate(sessionId) {
	let s = states.get(sessionId);
	if (s) return s;
	s = {
		sessionId,
		lastEventId: 0n,
		tail: [],
		listeners: /* @__PURE__ */ new Set(),
		diskPath: diskPathFor(sessionId)
	};
	states.set(sessionId, s);
	return s;
}
function persistEventSync(s, ev) {
	try {
		mkdirSync(dirname(s.diskPath), { recursive: true });
		appendFileSync(s.diskPath, JSON.stringify(ev) + "\n", "utf-8");
	} catch (err) {
		console.warn(`[session-log] persist failed for ${s.sessionId}: ${err?.message}`);
	}
}
/**
* Append an event to a session's log. Allocates the next monotonic
* `eventId`, writes the JSONL mirror, and then fans out to subscribers.
* Returns the event so the caller can use its `eventId`
* as a `parentEventId` reference for follow-up events.
*/
function appendEvent(args) {
	const s = getOrCreate(args.sessionId);
	s.lastEventId += 1n;
	const ev = {
		eventId: s.lastEventId.toString(),
		sessionId: args.sessionId,
		participantId: args.participantId,
		kind: args.kind,
		content: args.content,
		source: args.source,
		parentEventId: args.parentEventId,
		replyToEventId: args.replyToEventId,
		causedByEventId: args.causedByEventId,
		revisedFromEventId: args.revisedFromEventId,
		redactedEventId: args.redactedEventId,
		mentions: normalizeMentions(args.mentions),
		traceId: args.traceId,
		ts: (/* @__PURE__ */ new Date()).toISOString(),
		schemaVersion: SESSION_PROTOCOL_VERSION
	};
	s.tail.push(ev);
	if (s.tail.length > TAIL_LIMIT) s.tail.splice(0, s.tail.length - TAIL_LIMIT);
	persistEventSync(s, ev);
	for (const fn of s.listeners) try {
		fn(ev);
	} catch (err) {
		console.warn(`[session-log] listener threw: ${err?.message}`);
	}
	for (const fn of globalListeners) try {
		fn(ev);
	} catch (err) {
		console.warn(`[session-log] global listener threw: ${err?.message}`);
	}
	return ev;
}
/**
* Subscribe to live events for a session. Returns an unsubscribe
* function. The caller is responsible for calling it on disconnect.
*/
function subscribe(sessionId, fn) {
	const s = getOrCreate(sessionId);
	s.listeners.add(fn);
	return () => {
		s.listeners.delete(fn);
	};
}
const globalListeners = /* @__PURE__ */ new Set();
function subscribeAll(fn) {
	globalListeners.add(fn);
	return () => {
		globalListeners.delete(fn);
	};
}
/**
* Read events strictly after `sinceEventId`.
*
* Hot reconnects are served from the bounded in-memory tail. Cold reconnects
* and reconnects that need more than the hot tail fall back to the JSONL
* mirror so refresh never silently loses tool/media/reaction history.
*/
function readEventsSince(sessionId, sinceEventId = "0", limit = 200) {
	const max = normalizeLimit(limit);
	const s = states.get(sessionId);
	if (!s || s.tail.length === 0) return readDiskEventsSince(sessionId, sinceEventId, max);
	const since = eventIdToBigInt(sinceEventId);
	const firstTailId = eventIdToBigInt(s.tail[0]?.eventId);
	if (since + 1n < firstTailId) {
		const diskEvents = readDiskEventsSince(sessionId, sinceEventId, max);
		if (diskEvents.length > 0) return diskEvents;
	}
	const out = [];
	for (const e of s.tail) if (eventIdToBigInt(e.eventId) > since) {
		out.push(e);
		if (out.length >= max) break;
	}
	return out;
}
function readDiskEventsSince(sessionId, sinceEventId = "0", limit = 200) {
	const diskPath = diskPathFor(sessionId);
	if (!existsSync(diskPath)) return [];
	const max = normalizeLimit(limit);
	const since = eventIdToBigInt(sinceEventId);
	const out = [];
	try {
		const raw = readFileSync(diskPath, "utf-8");
		for (const line of raw.split("\n")) {
			const trimmed = line.trim();
			if (!trimmed) continue;
			try {
				const ev = JSON.parse(trimmed);
				if (eventIdToBigInt(ev.eventId) > since) {
					out.push(ev);
					if (out.length >= max) break;
				}
			} catch {}
		}
	} catch (err) {
		console.warn(`[session-log] replay failed for ${sessionId}: ${err?.message}`);
	}
	return out;
}
/**
* Cold-load the in-memory state for a session from its on-disk JSONL.
* Idempotent — safe to call before the first subscribe to populate the
* tail. If the file is missing, returns an empty session.
*/
async function hydrateSession(sessionId) {
	const s = getOrCreate(sessionId);
	if (s.lastEventId !== 0n) return;
	let raw;
	try {
		raw = await promises.readFile(s.diskPath, "utf-8");
	} catch {
		return;
	}
	for (const line of raw.split("\n")) {
		const trimmed = line.trim();
		if (!trimmed) continue;
		try {
			const ev = JSON.parse(trimmed);
			s.tail.push(ev);
			const id = eventIdToBigInt(ev.eventId);
			if (id > s.lastEventId) s.lastEventId = id;
		} catch {}
	}
	if (s.tail.length > TAIL_LIMIT) s.tail.splice(0, s.tail.length - TAIL_LIMIT);
}
function clearEventLogForSession(sessionId, opts = {}) {
	states.delete(sessionId);
	const path = diskPathFor(sessionId);
	if (!existsSync(path)) return {
		status: "missing",
		path
	};
	if (opts.hardDelete) {
		rmSync(path, { force: true });
		return {
			status: "removed",
			path
		};
	}
	const archivedPath = `${path}.${opts.reason || "deleted"}.${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}`;
	renameSync(path, archivedPath);
	return {
		status: "archived",
		path,
		archivedPath
	};
}

//#endregion
export { subscribe as a, readEventsSince as i, clearEventLogForSession as n, subscribeAll as o, hydrateSession as r, appendEvent as t };