/**
 * iteration-completion-reconciler.js — heal iter↔DB completion drift.
 *
 * Purpose: when .sf/runtime/autonomous-solver/iterations.jsonl shows
 * outcome=complete for a unit, ensure the DB tasks row matches. If the row
 * is status=pending OR completed_at is null OR an older ts, update it to
 * match the iter timestamp.
 *
 * Consumer: UokGateRegistry (type=verification, id=iter-completion-reconciler).
 *
 * Failure boundary: read-only on iter; write-only on tasks (status,
 * completed_at, verification_status). Slice/milestone flip handled by
 * existing sweepers once tasks are correct.
 */
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";

// ─── Constants ─────────────────────────────────────────────────────────────

const DEFAULT_MAX_ITER_ENTRIES = 500;
const DEFAULT_MAX_AGE_MS = 3_600_000; // 1 hour — older drift is operator territory
const ITER_JSONL_SUBPATH = join(
	"runtime",
	"autonomous-solver",
	"iterations.jsonl",
);
const SELF_FEEDBACK_SUBPATH = join("self-feedback.jsonl");

// ─── Helpers ───────────────────────────────────────────────────────────────

function sfRootDir(basePath) {
	return join(basePath, ".sf");
}

function iterPath(basePath) {
	return join(sfRootDir(basePath), ITER_JSONL_SUBPATH);
}

function selfFeedbackPath(basePath) {
	return join(sfRootDir(basePath), SELF_FEEDBACK_SUBPATH);
}

function newId() {
	const ts = Date.now().toString(36);
	const rnd = Math.random().toString(36).slice(2, 8);
	return `sf-${ts}-${rnd}`;
}

/**
 * Parse "M048/S02/T01" into { milestoneId, sliceId, taskId }.
 * Returns null if the string doesn't have all three segments (skip non-task units).
 */
function parseTaskUnitId(unitId) {
	if (typeof unitId !== "string") return null;
	const parts = unitId.split("/");
	if (parts.length < 3) return null;
	const [milestoneId, sliceId, taskId] = parts;
	// Must look like M\d+ / S\d+ / T\d+ (or alphanumeric suffix)
	if (!milestoneId || !sliceId || !taskId) return null;
	// Skip research-slice units: no T-prefixed part
	if (!taskId.match(/^T/i)) return null;
	return { milestoneId, sliceId, taskId };
}

/**
 * Read iterations.jsonl, parse lines (most-recent-last), return raw objects.
 * Silently skips malformed lines.
 */
function readIterEntries(basePath) {
	const path = iterPath(basePath);
	if (!existsSync(path)) return [];
	const raw = readFileSync(path, "utf-8");
	const lines = raw.split("\n").filter((l) => l.trim());
	const entries = [];
	for (const line of lines) {
		try {
			const parsed = JSON.parse(line);
			if (parsed && typeof parsed === "object") entries.push(parsed);
		} catch {
			/* skip malformed lines */
		}
	}
	return entries;
}

/**
 * From a list of iter entries (all outcomes), find the latest entry per unitId
 * where outcome === "complete". Returns Map<unitId, entry>.
 *
 * Scans backward (tail-first) so we find the latest per unit quickly.
 */
function latestCompleteByUnit(entries, maxEntries, maxAgeMs) {
	const cutoffTs = Date.now() - maxAgeMs;
	// Take the last maxEntries lines
	const window = entries.slice(-maxEntries);
	const byUnit = new Map();
	// Iterate in reverse to find latest first
	for (let i = window.length - 1; i >= 0; i--) {
		const entry = window[i];
		if (entry.outcome !== "complete") continue;
		const unitId = entry.unitId;
		if (typeof unitId !== "string") continue;
		// Age filter on entry timestamp
		const entryMs = entry.ts ? new Date(entry.ts).getTime() : 0;
		if (!entryMs || entryMs < cutoffTs) continue;
		// Only store the first (latest) complete entry per unit in this backward pass
		if (!byUnit.has(unitId)) {
			byUnit.set(unitId, entry);
		}
	}
	return byUnit;
}

/**
 * Append a self-feedback entry to .sf/self-feedback.jsonl (non-fatal; swallows errors).
 */
function appendSelfFeedback(basePath, unitId, iterTs) {
	try {
		const path = selfFeedbackPath(basePath);
		const dir = dirname(path);
		if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
		const entry = {
			id: newId(),
			ts: new Date().toISOString(),
			kind: "iter-vs-db-drift-recovered",
			severity: "low",
			blocking: false,
			summary: `iter-vs-db drift auto-healed for ${unitId}`,
			evidence: `iterations.jsonl showed outcome=complete at ${iterTs}; DB row was behind`,
			unitId,
			iterTs,
		};
		appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf-8");
	} catch {
		/* non-fatal */
	}
}

// ─── Core reconciler ───────────────────────────────────────────────────────

/**
 * Reconcile iter↔DB completion drift.
 *
 * @param {string} basePath — project root (directory containing .sf/)
 * @param {object} [options]
 * @param {number}  [options.maxIterEntries=500] — tail window size
 * @param {number}  [options.maxAgeMs=3600000]   — ignore iter entries older than this
 * @param {boolean} [options.dryRun=false]        — skip writes when true
 * @param {object}  [options.db]                  — optional pre-opened adapter (tests inject this)
 * @returns {{ reconciled: Array, totalChecked: number, durationMs: number }}
 */
export async function reconcileIterCompletions(basePath, options = {}) {
	const t0 = Date.now();
	const maxEntries = options.maxIterEntries ?? DEFAULT_MAX_ITER_ENTRIES;
	const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
	const dryRun = options.dryRun === true;

	// Resolve the DB adapter: prefer injected (for tests), fall back to module singleton
	let db = options.db ?? null;
	if (!db) {
		try {
			const { _getAdapter } = await import("./sf-db-core.js");
			db = _getAdapter();
		} catch {
			/* DB not available */
		}
	}

	const reconciled = [];

	// 1. Read iterations
	const entries = readIterEntries(basePath);
	const completeByUnit = latestCompleteByUnit(entries, maxEntries, maxAgeMs);

	// 2. For each unit with a complete iter, check the DB row
	let totalChecked = 0;
	for (const [unitId, iterEntry] of completeByUnit) {
		const parsed = parseTaskUnitId(unitId);
		if (!parsed) continue; // non-task unit — skip
		totalChecked++;

		const { milestoneId, sliceId, taskId } = parsed;
		const iterTs = iterEntry.ts ?? null;

		if (!db) continue; // no DB open — nothing to reconcile

		// Query the task row
		let row;
		try {
			row = db
				.prepare(
					"SELECT status, completed_at FROM tasks WHERE milestone_id = :m AND slice_id = :s AND id = :t",
				)
				.get({ ":m": milestoneId, ":s": sliceId, ":t": taskId });
		} catch {
			continue; // table may not exist in tests without full schema — skip
		}

		if (!row) continue; // no matching row — nothing to fix

		const dbStatus = row.status;
		const dbCompletedAt = row.completed_at;

		// Determine whether we need to update:
		// - DB is not "complete", OR
		// - DB is "complete" but completed_at is null or OLDER than iter ts
		const needsUpdate = (() => {
			if (dbStatus !== "complete") return true;
			if (!dbCompletedAt) return true;
			if (!iterTs) return false;
			// DB ahead of iter? Don't touch it.
			const dbMs = new Date(dbCompletedAt).getTime();
			const iterMs = new Date(iterTs).getTime();
			return iterMs > dbMs;
		})();

		if (!needsUpdate) continue;

		if (!dryRun) {
			try {
				db.prepare(
					`UPDATE tasks SET status = 'complete', completed_at = :ca, verification_status = 'all_pass'
					 WHERE milestone_id = :m AND slice_id = :s AND id = :t`,
				).run({
					":ca": iterTs ?? new Date().toISOString(),
					":m": milestoneId,
					":s": sliceId,
					":t": taskId,
				});
				appendSelfFeedback(basePath, unitId, iterTs);
			} catch {
				continue; // non-fatal per-row failure
			}
		}

		reconciled.push({
			unitId,
			oldStatus: dbStatus,
			newStatus: "complete",
			source: "iterations.jsonl",
		});
	}

	return {
		reconciled,
		totalChecked,
		durationMs: Date.now() - t0,
	};
}

// ─── UOK Gate wrapper ──────────────────────────────────────────────────────

export const iterCompletionReconcilerGate = {
	id: "iter-completion-reconciler",
	type: "verification",
	async execute(ctx) {
		const result = await reconcileIterCompletions(ctx.basePath, ctx.options);
		return result.reconciled.length === 0
			? {
					outcome: "pass",
					failureClass: null,
					rationale: `iter-vs-db reconciled: 0 of ${result.totalChecked} tasks needed update`,
				}
			: {
					outcome: "manual-attention",
					failureClass: "verification",
					rationale: `iter-vs-db reconciled ${result.reconciled.length} of ${result.totalChecked} drift(ed) tasks`,
					findings: result.reconciled,
				};
	},
};
