import { closeSync, fsyncSync, openSync, renameSync, statSync, writeSync } from "node:fs";
import type { LogRecord, LogSink } from "@dispatch/kernel";

// --- Pure core (no I/O) ---

/**
 * Serialize a LogRecord to exactly one NDJSON line (record JSON + newline).
 * Pure function — no I/O, no side effects.
 */
export function serialize(record: LogRecord): string {
	return `${JSON.stringify(record)}\n`;
}

// --- Imperative shell (fs edge) ---

/** Injectable fs operations — confined to the edge. */
export interface FsOps {
	readonly open: (path: string) => number;
	readonly write: (fd: number, data: string) => void;
	readonly close: (fd: number) => void;
	readonly rename: (oldPath: string, newPath: string) => void;
	readonly statSize: (path: string) => number;
	readonly fsync: (fd: number) => void;
}

/** Clock injection for fsync interval. */
export interface ClockOps {
	readonly now: () => number;
}

export interface JournalSinkOpts {
	readonly path: string;
	readonly maxBytes?: number;
	readonly fsync?: "periodic" | "none";
	readonly fs?: FsOps;
	readonly clock?: ClockOps;
}

const DEFAULT_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
const FSYNC_INTERVAL_MS = 5_000; // 5 seconds

/**
 * Create a LogSink that durably appends each record to the journal file
 * as one NDJSON line. Fail-safe: write errors drop + warn, never throw.
 * Rotates when file exceeds maxBytes (rename → .1, reopen fresh).
 */
export function createJournalSink(opts: JournalSinkOpts): LogSink {
	const filePath = opts.path;
	const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
	const syncMode = opts.fsync ?? "periodic";
	const fs = opts.fs ?? createDefaultFsOps();
	const clock = opts.clock ?? { now: () => Date.now() };

	const NO_FD = -1;
	let fd: number;
	let bytesWritten: number;
	try {
		fd = fs.open(filePath);
		bytesWritten = fs.statSize(filePath);
	} catch {
		fd = NO_FD;
		bytesWritten = 0;
	}
	let lastFsyncAt = clock.now();

	function tryOpen(): boolean {
		if (fd !== NO_FD) return true;
		try {
			fd = fs.open(filePath);
			bytesWritten = 0;
			return true;
		} catch {
			return false;
		}
	}

	function rotate(): void {
		if (fd !== NO_FD) {
			try {
				fs.close(fd);
			} catch {
				// Ignore close errors during rotation.
			}
		}
		const rotatedPath = `${filePath}.1`;
		try {
			fs.rename(filePath, rotatedPath);
		} catch {
			// If rename fails, just truncate by reopening.
		}
		try {
			fd = fs.open(filePath);
		} catch {
			fd = NO_FD;
		}
		bytesWritten = 0;
	}

	function maybeFsync(): void {
		if (syncMode !== "periodic" || fd === NO_FD) return;
		const now = clock.now();
		if (now - lastFsyncAt >= FSYNC_INTERVAL_MS) {
			try {
				fs.fsync(fd);
			} catch {
				// Swallow — fail-safe.
			}
			lastFsyncAt = now;
		}
	}

	const sink: LogSink = {
		emit(record: LogRecord): void {
			try {
				if (!tryOpen()) {
					console.warn("[journal-sink] cannot open journal, dropping record");
					return;
				}

				const line = serialize(record);
				const lineBytes = Buffer.byteLength(line, "utf8");

				if (bytesWritten + lineBytes > maxBytes) {
					rotate();
					if (fd === NO_FD) {
						console.warn("[journal-sink] rotation failed, dropping record");
						return;
					}
				}

				fs.write(fd, line);
				bytesWritten += lineBytes;
				maybeFsync();
			} catch (err) {
				// Fail-safe: drop + warn, never throw to the caller (D3/D7).
				console.warn("[journal-sink] write failed, dropping record:", err);
			}
		},
	};

	return sink;
}

// --- Default fs ops (real Bun/Node I/O) ---

function createDefaultFsOps(): FsOps {
	return {
		open(path: string): number {
			return openSync(path, "a");
		},
		write(fd: number, data: string): void {
			writeSync(fd, data);
		},
		close(fd: number): void {
			closeSync(fd);
		},
		rename(oldPath: string, newPath: string): void {
			renameSync(oldPath, newPath);
		},
		statSize(path: string): number {
			try {
				return statSync(path).size;
			} catch {
				return 0;
			}
		},
		fsync(fd: number): void {
			fsyncSync(fd);
		},
	};
}
