// Writes typed trace events to .sf/traces/<traceId>.jsonl
// Updates a `latest` symlink for easy tail -f access.
import {
	appendFileSync,
	closeSync,
	existsSync,
	lstatSync,
	mkdirSync,
	openSync,
	readdirSync,
	readFileSync,
	statSync,
	symlinkSync,
	unlinkSync,
} from "node:fs";
import { join } from "node:path";
import { sfRoot } from "../paths.js";

// Longest read window currently used by any trace consumer
// (sf-db-gates.js:391 reads 30 days). Anything older than this is never
// read and just consumes disk.
const TRACE_RETENTION_DAYS_DEFAULT = 30;
const MS_PER_DAY = 24 * 60 * 60 * 1000;

function tracesDir(basePath) {
	return join(sfRoot(basePath), "traces");
}

function tracePath(basePath, traceId) {
	return join(tracesDir(basePath), `${traceId}.jsonl`);
}

export function appendTraceEvent(basePath, traceId, event) {
	if (!basePath || !traceId) return;
	try {
		appendTraceEventRequired(basePath, traceId, event);
	} catch {
		// trace writes must never break orchestration
	}
}

/**
 * Append a trace event and throw when it cannot be persisted.
 *
 * Purpose: let UOK control-plane flows fail closed when their required gate
 * record cannot be written. Best-effort orchestration should keep using
 * appendTraceEvent; supervised mutation paths use this function so UOK is not
 * silently bypassed.
 *
 * Consumer: headless triage --apply gate emission.
 */
export function appendTraceEventRequired(basePath, traceId, event) {
	if (!basePath || !traceId) {
		throw new Error("appendTraceEventRequired requires basePath and traceId");
	}
	const dir = tracesDir(basePath);
	mkdirSync(dir, { recursive: true });
	const path = tracePath(basePath, traceId);
	const line = JSON.stringify({ ts: new Date().toISOString(), ...event });
	if (!existsSync(path)) closeSync(openSync(path, "a"));
	appendFileSync(path, `${line}\n`, "utf-8");
	// Update latest symlink
	const latestPath = join(dir, "latest");
	try {
		unlinkSync(latestPath);
	} catch {
		/* ok if missing */
	}
	symlinkSync(`${traceId}.jsonl`, latestPath);
}

/**
 * Prune .sf/traces/*.jsonl files older than retentionDays.
 *
 * Why: per-flow trace files accumulate one-per-dispatch and are never
 * cleaned. The longest analyzer window today is 30 days
 * (sf-db-gates.js:391); anything older is never read and just consumes
 * disk. The `latest` symlink is preserved unconditionally so the
 * tail-friendly pointer keeps working.
 *
 * Consumer: session-start hook (idempotent, fast, best-effort).
 *
 * @param {string} basePath
 * @param {object} [opts]
 * @param {number} [opts.retentionDays=30]
 * @param {number} [opts.maxDeletePerCall=1000] - safety cap so a runaway
 *   directory doesn't make startup slow.
 * @returns {{ scanned: number, pruned: number, errors: number }}
 */
export function pruneStaleTraces(basePath, opts = {}) {
	const retentionDays = Number(
		opts.retentionDays ?? TRACE_RETENTION_DAYS_DEFAULT,
	);
	const maxDeletePerCall = Math.max(1, Number(opts.maxDeletePerCall ?? 1000));
	const result = { scanned: 0, pruned: 0, errors: 0 };
	if (!basePath || typeof basePath !== "string") return result;
	let dir;
	try {
		dir = tracesDir(basePath);
	} catch {
		return result;
	}
	if (!existsSync(dir)) return result;
	const cutoff = Date.now() - retentionDays * MS_PER_DAY;
	let entries;
	try {
		entries = readdirSync(dir);
	} catch {
		return result;
	}
	for (const name of entries) {
		if (result.pruned >= maxDeletePerCall) break;
		if (name === "latest") continue;
		if (!name.endsWith(".jsonl")) continue;
		const path = join(dir, name);
		result.scanned += 1;
		try {
			// lstat so we don't follow a symlink (defensive — there shouldn't
			// be any besides `latest`, but never silently chase).
			const stat = lstatSync(path);
			if (!stat.isFile()) continue;
			if (stat.mtimeMs >= cutoff) continue;
			unlinkSync(path);
			result.pruned += 1;
		} catch {
			result.errors += 1;
		}
	}
	return result;
}

export function readTraceEvents(basePath, type, windowHours = 24) {
	// Read all trace files modified within windowHours, filter by event type
	// Returns array of matching events
	const dir = tracesDir(basePath);
	if (!existsSync(dir)) return [];
	const cutoff = Date.now() - windowHours * 60 * 60 * 1000;
	const results = [];
	let files;
	try {
		files = readdirSync(dir).filter(
			(f) => f.endsWith(".jsonl") && f !== "latest",
		);
	} catch {
		return [];
	}
	for (const file of files) {
		try {
			const filePath = join(dir, file);
			if (statSync(filePath).mtimeMs < cutoff) continue;
			const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
			for (const line of lines) {
				try {
					const ev = JSON.parse(line);
					if (!type || ev.type === type) {
						if (!ev.ts || new Date(ev.ts).getTime() >= cutoff) results.push(ev);
					}
				} catch {
					/* skip malformed lines */
				}
			}
		} catch {
			/* skip unreadable files */
		}
	}
	return results;
}
