import type { LiveQueryEvent, QueryRow } from "@/lib/types";

/** A live stream row; `key` is a client-side monotonic counter (SSE frames carry no id). */
export interface LiveRow extends LiveQueryEvent {
	key: number;
}

export const RING_CAPACITY = 500;

/** Prepend `row` (rows are newest-first) and drop the oldest beyond `capacity`. */
export function pushRow(rows: LiveRow[], row: LiveRow, capacity: number = RING_CAPACITY): LiveRow[] {
	const next = [row, ...rows];
	return next.length > capacity ? next.slice(0, capacity) : next;
}

// `since` on GET /api/queries is inclusive, so the re-sync fetch returns the
// last-seen row(s) again; live rows have no id, so identity is this tuple.
function signature(row: LiveQueryEvent): string {
	return `${row.ts}|${row.domain}|${row.client_ip}|${row.qtype ?? -1}|${row.blocked}|${row.upstream}`;
}

/**
 * Merge rows fetched for a reconnect gap (newest-first, from GET /api/queries)
 * into the buffer. Rows already present are skipped; `missed` counts what was
 * actually added. The result stays newest-first (stable sort by ts) and capped.
 */
export function mergeGap(
	rows: LiveRow[],
	fetched: QueryRow[],
	nextKey: () => number,
	capacity: number = RING_CAPACITY,
): { rows: LiveRow[]; missed: number } {
	const seen = new Set(rows.map(signature));
	const added: LiveRow[] = [];
	for (const row of fetched) {
		const event: LiveQueryEvent = {
			ts: row.ts,
			domain: row.domain,
			client_ip: row.client_ip,
			qtype: row.qtype,
			blocked: row.blocked,
			block_reason: row.block_reason,
			response_time_us: row.response_time_us,
			cache_hit: row.cache_hit,
			upstream: row.upstream,
		};
		if (seen.has(signature(event))) continue;
		added.push({ ...event, key: nextKey() });
	}
	if (added.length === 0) return { rows, missed: 0 };
	const merged = [...added, ...rows].sort((a, b) => b.ts - a.ts).slice(0, capacity);
	return { rows: merged, missed: added.length };
}
