import { randomBytes } from "node:crypto";
import {
	promises as fs,
	mkdirSync,
	renameSync,
	unlinkSync,
	writeFileSync,
} from "node:fs";
import { dirname } from "node:path";
import { isMainThread } from "node:worker_threads";
import { getErrorMessage } from "./error-utils.js";

const TRANSIENT_LOCK_ERROR_CODES = new Set(["EBUSY", "EPERM", "EACCES"]);
const MAX_RENAME_ATTEMPTS = 5;
function defaultTempPath(filePath) {
	return filePath + `.tmp.${randomBytes(4).toString("hex")}`;
}
function computeRetryDelayMs(attempt) {
	const base = 8 * attempt;
	const jitter = randomBytes(1)[0] % 5;
	return base + jitter;
}
export function delay(ms) {
	return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Sleep synchronously using a busy-wait spin loop.
 *
 * WARNING: This blocks the event loop. Must only be called from Worker threads
 * or contexts where blocking is safe. Calling from the main thread will freeze
 * the process, especially during async I/O.
 */
function sleepSync(ms) {
	// Runtime guard: warn if called from main thread.
	if (isMainThread) {
		console.warn(
			"sleepSync: blocking the main thread event loop. Consider using an async delay instead.",
		);
	}
	const deadline = Date.now() + ms;
	while (Date.now() < deadline) {
		// spin
	}
}
function normalizeErrnoCode(error) {
	if (error && typeof error === "object" && "code" in error) {
		const code = error.code;
		return typeof code === "string" ? code : undefined;
	}
	return undefined;
}
function isTransientLockError(error) {
	const code = normalizeErrnoCode(error);
	return typeof code === "string" && TRANSIENT_LOCK_ERROR_CODES.has(code);
}
function buildAtomicWriteError(filePath, attempts, errors) {
	const lastError = errors[errors.length - 1];
	const code = normalizeErrnoCode(lastError) ?? "UNKNOWN";
	const messages = errors.map(
		(e, i) =>
			`  attempt ${i + 1}: [${normalizeErrnoCode(e) ?? "UNKNOWN"}] ${getErrorMessage(e)}`,
	);
	const wrapped = new Error(
		`Atomic write to ${filePath} failed after ${attempts} attempts:\n${messages.join("\n")}`,
	);
	wrapped.code = code;
	if (lastError instanceof Error && "stack" in lastError && lastError.stack) {
		wrapped.stack = lastError.stack;
	}
	return wrapped;
}
async function cleanupTempFileAsync(tmpPath, ops) {
	try {
		await ops.unlink(tmpPath);
	} catch {
		// Best-effort cleanup only.
	}
}
function cleanupTempFileSync(tmpPath, ops) {
	try {
		ops.unlink(tmpPath);
	} catch {
		// Best-effort cleanup only.
	}
}
/** @internal Exported for retry/cleanup tests. */
export async function atomicWriteAsyncWithOps(
	filePath,
	content,
	encoding = "utf-8",
	ops,
) {
	await ops.mkdir(dirname(filePath), { recursive: true });
	const tmpPath = ops.createTempPath?.(filePath) ?? defaultTempPath(filePath);
	await ops.writeFile(tmpPath, content, encoding);
	const errors = [];
	let attempts = 0;
	for (attempts = 1; attempts <= MAX_RENAME_ATTEMPTS; attempts++) {
		try {
			await ops.rename(tmpPath, filePath);
			return;
		} catch (error) {
			errors.push(error);
			if (!isTransientLockError(error) || attempts === MAX_RENAME_ATTEMPTS) {
				break;
			}
			await ops.sleep(computeRetryDelayMs(attempts));
		}
	}
	await cleanupTempFileAsync(tmpPath, ops);
	throw buildAtomicWriteError(filePath, attempts, errors);
}
/** @internal Exported for retry/cleanup tests. */
export function atomicWriteSyncWithOps(
	filePath,
	content,
	encoding = "utf-8",
	ops,
) {
	ops.mkdir(dirname(filePath), { recursive: true });
	const tmpPath = ops.createTempPath?.(filePath) ?? defaultTempPath(filePath);
	ops.writeFile(tmpPath, content, encoding);
	const errors = [];
	let attempts = 0;
	for (attempts = 1; attempts <= MAX_RENAME_ATTEMPTS; attempts++) {
		try {
			ops.rename(tmpPath, filePath);
			return;
		} catch (error) {
			errors.push(error);
			if (!isTransientLockError(error) || attempts === MAX_RENAME_ATTEMPTS) {
				break;
			}
			ops.sleep(computeRetryDelayMs(attempts));
		}
	}
	cleanupTempFileSync(tmpPath, ops);
	throw buildAtomicWriteError(filePath, attempts, errors);
}
const DEFAULT_ASYNC_OPS = {
	mkdir: async (path, options) => {
		await fs.mkdir(path, options);
	},
	writeFile: (path, content, encoding) => fs.writeFile(path, content, encoding),
	rename: (from, to) => fs.rename(from, to),
	unlink: (path) => fs.unlink(path),
	sleep: delay,
};
const DEFAULT_SYNC_OPS = {
	mkdir: (path, options) => mkdirSync(path, options),
	writeFile: (path, content, encoding) =>
		writeFileSync(path, content, encoding),
	rename: (from, to) => renameSync(from, to),
	unlink: (path) => unlinkSync(path),
	sleep: sleepSync,
};
/**
 * Atomically writes content to a file by writing to a temp file first,
 * then renaming. Prevents partial/corrupt files on crash.
 */
export function atomicWriteSync(filePath, content, encoding = "utf-8") {
	atomicWriteSyncWithOps(filePath, content, encoding, DEFAULT_SYNC_OPS);
}
/**
 * Async variant of atomicWriteSync. Atomically writes content to a file
 * by writing to a temp file first, then renaming.
 */
export async function atomicWriteAsync(filePath, content, encoding = "utf-8") {
	return atomicWriteAsyncWithOps(
		filePath,
		content,
		encoding,
		DEFAULT_ASYNC_OPS,
	);
}
