import { createLogger } from "../../../utils/logger/index.js";
import { init_logger } from "../../../utils/logger.js";
import { DREAMING_DIR_RELATIVE } from "./constants.js";
import { buildEntryKey, isoDay, normalizeMemoryPath, normalizeSnippetForHash, snippetHash } from "./utils.js";
import { bumpEntryPhaseSignal, loadDreamingStore, saveDreamingStore } from "./short-term-store.js";
import "./last-run.js";
import fs from "node:fs/promises";
import path from "node:path";
//#region src/agent/memory/dreaming/light-sweep.ts
init_logger();
const log = createLogger("Dreaming:Light");
function isDailyMemoryFile(filename) {
	return /^\d{4}-\d{2}-\d{2}\.md$/i.test(filename);
}
/**
* Cosine-ish similarity via character n-gram overlap.
* Fast and deterministic — no embeddings required.
*/
function trigramSimilarity(textA, textB) {
	const normalizedA = normalizeSnippetForHash(textA);
	const normalizedB = normalizeSnippetForHash(textB);
	if (!normalizedA || !normalizedB) return 0;
	if (normalizedA === normalizedB) return 1;
	const gramsA = buildTrigramSet(normalizedA);
	const gramsB = buildTrigramSet(normalizedB);
	if (gramsA.size === 0 || gramsB.size === 0) return 0;
	let intersection = 0;
	for (const gram of gramsA) if (gramsB.has(gram)) intersection += 1;
	return intersection / Math.sqrt(gramsA.size * gramsB.size);
}
function buildTrigramSet(text) {
	const grams = /* @__PURE__ */ new Set();
	for (let i = 0; i <= text.length - 3; i++) grams.add(text.slice(i, i + 3));
	return grams;
}
function resolveConfig(overrides) {
	return {
		enabled: overrides?.enabled === true,
		cron: typeof overrides?.cron === "string" ? overrides.cron : "0 */6 * * *",
		lookbackDays: Math.max(1, Math.floor(Number(overrides?.lookbackDays) || 2)),
		limit: Math.max(0, Math.floor(Number(overrides?.limit) || 100)),
		dedupeSimilarity: Math.max(0, Math.min(1, Number(overrides?.dedupeSimilarity) || .9))
	};
}
/**
* Scan recent daily memory files (memory/YYYY-MM-DD.md) and collect signal
* entries into the short-term store. Deduplicates near-identical snippets
* using trigram similarity.
*
* This is the "light sleep" phase: fast, frequent, cheap.
*/
async function runLightSweep(params) {
	const cfg = resolveConfig(params.config);
	const now = params.now ?? /* @__PURE__ */ new Date();
	const startedAt = now.toISOString();
	const runId = `light:${startedAt}:${process.pid}`;
	const startMs = Date.now();
	if (!cfg.enabled) {
		await writeLastRun(params.workspaceDir, {
			runId,
			startedAt,
			cfg,
			ok: true,
			reason: "light sweep disabled",
			startMs,
			light: {
				scannedEntries: 0,
				newSignals: 0,
				deduped: 0
			}
		});
		return {
			ok: true,
			reason: "light sweep disabled",
			scannedEntries: 0,
			newSignals: 0,
			deduped: 0
		};
	}
	try {
		const recentFiles = await listRecentDailyFiles(path.join(params.workspaceDir, "memory"), cfg.lookbackDays, now);
		if (recentFiles.length === 0) {
			await writeLastRun(params.workspaceDir, {
				runId,
				startedAt,
				cfg,
				ok: true,
				reason: "no recent daily files",
				startMs,
				light: {
					scannedEntries: 0,
					newSignals: 0,
					deduped: 0
				}
			});
			return {
				ok: true,
				reason: "no recent daily files",
				scannedEntries: 0,
				newSignals: 0,
				deduped: 0
			};
		}
		const allChunks = [];
		for (const filePath of recentFiles) {
			const chunks = await parseFileChunks(filePath, params.workspaceDir);
			allChunks.push(...chunks);
		}
		if (allChunks.length === 0) {
			await writeLastRun(params.workspaceDir, {
				runId,
				startedAt,
				cfg,
				ok: true,
				reason: "no chunks found in daily files",
				startMs,
				light: {
					scannedEntries: 0,
					newSignals: 0,
					deduped: 0
				}
			});
			return {
				ok: true,
				reason: "no chunks found in daily files",
				scannedEntries: 0,
				newSignals: 0,
				deduped: 0
			};
		}
		const { unique, deduped } = deduplicateChunks(allChunks, cfg.dedupeSimilarity);
		const capped = unique.slice(0, cfg.limit);
		const { store } = await loadDreamingStore({ workspaceDir: params.workspaceDir });
		let newSignals = 0;
		const dayBucket = isoDay(now);
		const nowIso = now.toISOString();
		for (const chunk of capped) {
			const key = buildEntryKey({
				path: chunk.path,
				startLine: chunk.startLine,
				endLine: chunk.endLine
			});
			const existing = store.entries[key];
			if (existing) {
				bumpEntryPhaseSignal(existing, "lightHits");
				bumpEntryPhaseSignal(existing, "dailyCount");
				existing.snippet = chunk.text.slice(0, 360);
				existing.lastRecalledAt = nowIso;
			} else {
				store.entries[key] = {
					key,
					path: chunk.path,
					startLine: chunk.startLine,
					endLine: chunk.endLine,
					snippet: chunk.text.slice(0, 360),
					recallCount: 0,
					dailyCount: 1,
					groundedCount: 0,
					lightHits: 1,
					remHits: 0,
					phaseHitCount: 1,
					totalSignalCount: 1,
					totalScore: 0,
					maxScore: 0,
					queryHashes: [],
					recallDays: [dayBucket],
					firstRecalledAt: nowIso,
					lastRecalledAt: nowIso
				};
				newSignals += 1;
			}
		}
		store.updatedAt = nowIso;
		await saveDreamingStore({
			workspaceDir: params.workspaceDir,
			store
		});
		log.info({
			workspaceDir: params.workspaceDir,
			scanned: allChunks.length,
			newSignals,
			deduped
		}, "Light sweep complete");
		await writeLastRun(params.workspaceDir, {
			runId,
			startedAt,
			cfg,
			ok: true,
			reason: "light sweep complete",
			startMs,
			light: {
				scannedEntries: allChunks.length,
				newSignals,
				deduped
			}
		});
		return {
			ok: true,
			reason: "light sweep complete",
			scannedEntries: allChunks.length,
			newSignals,
			deduped
		};
	} catch (err) {
		const errorMessage = err instanceof Error ? err.message : String(err);
		log.error({
			err,
			errorMessage,
			workspaceDir: params.workspaceDir
		}, `Light sweep failed: ${errorMessage}`);
		await writeLastRun(params.workspaceDir, {
			runId,
			startedAt,
			cfg,
			ok: false,
			reason: `light sweep error: ${errorMessage}`,
			startMs,
			light: {
				scannedEntries: 0,
				newSignals: 0,
				deduped: 0
			},
			errorMessage
		}).catch(() => void 0);
		return {
			ok: false,
			reason: errorMessage,
			scannedEntries: 0,
			newSignals: 0,
			deduped: 0
		};
	}
}
async function listRecentDailyFiles(memoryDir, lookbackDays, now) {
	let entries;
	try {
		entries = await fs.readdir(memoryDir);
	} catch (err) {
		if (err?.code === "ENOENT") return [];
		throw err;
	}
	const cutoff = new Date(now);
	cutoff.setDate(cutoff.getDate() - lookbackDays);
	const cutoffDay = isoDay(cutoff);
	return entries.filter((name) => isDailyMemoryFile(name)).filter((name) => {
		return name.replace(/\.md$/i, "") >= cutoffDay;
	}).sort().map((name) => path.join(memoryDir, name));
}
async function parseFileChunks(filePath, workspaceDir) {
	let raw;
	try {
		raw = await fs.readFile(filePath, "utf-8");
	} catch {
		return [];
	}
	const relativePath = normalizeMemoryPath(path.relative(workspaceDir, filePath));
	const lines = raw.split(/\r?\n/);
	const chunks = [];
	let chunkStartLine = 1;
	let currentLines = [];
	for (let i = 0; i < lines.length; i++) {
		const trimmed = lines[i].trim();
		if (trimmed.length === 0) {
			if (currentLines.length > 0) {
				const text = currentLines.join(" ").replace(/\s+/g, " ").trim();
				if (text.length >= 20) chunks.push({
					path: relativePath,
					startLine: chunkStartLine,
					endLine: chunkStartLine + currentLines.length - 1,
					text: text.slice(0, 360),
					hash: snippetHash(text)
				});
				currentLines = [];
			}
			chunkStartLine = i + 2;
		} else {
			if (currentLines.length === 0) chunkStartLine = i + 1;
			currentLines.push(trimmed);
		}
	}
	if (currentLines.length > 0) {
		const text = currentLines.join(" ").replace(/\s+/g, " ").trim();
		if (text.length >= 20) chunks.push({
			path: relativePath,
			startLine: chunkStartLine,
			endLine: chunkStartLine + currentLines.length - 1,
			text: text.slice(0, 360),
			hash: snippetHash(text)
		});
	}
	return chunks;
}
function deduplicateChunks(chunks, threshold) {
	const unique = [];
	let deduped = 0;
	for (const chunk of chunks) if (unique.some((existing) => existing.hash === chunk.hash || trigramSimilarity(existing.text, chunk.text) >= threshold)) deduped += 1;
	else unique.push(chunk);
	return {
		unique,
		deduped
	};
}
async function writeLastRun(workspaceDir, params) {
	const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
	const durationMs = Math.max(0, Date.now() - params.startMs);
	const lastRun = {
		version: 2,
		phase: "light",
		runId: params.runId,
		startedAt: params.startedAt,
		finishedAt,
		durationMs,
		ok: params.ok,
		reason: params.reason,
		config: params.cfg,
		light: params.light,
		...params.errorMessage ? { errorMessage: params.errorMessage } : {}
	};
	const lastRunPath = path.join(workspaceDir, DREAMING_DIR_RELATIVE, "last-run-light.json");
	await fs.mkdir(path.dirname(lastRunPath), { recursive: true });
	const tmp = `${lastRunPath}.${process.pid}.${Date.now()}.tmp`;
	await fs.writeFile(tmp, `${JSON.stringify(lastRun, null, 2)}\n`, "utf-8");
	await fs.rename(tmp, lastRunPath);
}
//#endregion
export { runLightSweep };

//# sourceMappingURL=light-sweep.js.map