/**
 * Singularity Memory Sync Scheduler (Tier 1.2 Phase 2)
 *
 * Purpose: batch and schedule memory syncs to Singularity Memory with retry logic.
 * Enables asynchronous, non-blocking federation while maintaining local autonomy.
 *
 * Consumer: memory-store.js (after createMemory) and unit-runtime.js (session-end flush).
 *
 * Design:
 * - Fire-and-forget queue: memories are added via queueMemorySync()
 * - Batching: syncs are batched (default 50 items or 5s timeout)
 * - Retry logic: exponential backoff (1s → 2s → 4s, max 3 retries)
 * - Graceful degradation: failed syncs log warning, don't block unit completion
 * - Session-end flush: before unit completes, pending syncs are flushed (best-effort)
 */

import { delay } from "./atomic-write.js";
import { getErrorMessage } from "./error-utils.js";
import { syncMemoryToSm } from "./sm-client.js";

/**
 * Global sync scheduler state.
 * Keyed by projectId to support multiple concurrent projects.
 */
const globalState = {};

function getProjectScheduler(projectId) {
	if (!globalState[projectId]) {
		globalState[projectId] = {
			queue: [], // Array of { id, memory }
			batchTimer: null,
			flushing: false,
			stats: {
				queued: 0,
				synced: 0,
				failed: 0,
			},
		};
	}
	return globalState[projectId];
}

/**
 * Queue a memory for sync to Singularity Memory.
 * Returns immediately; actual sync happens asynchronously.
 *
 * @param {string} projectId - Project identifier
 * @param {string} memoryId - Unique memory identifier
 * @param {object} memory - Memory object { type, content, projectId, ... }
 * @param {object} opts - Optional { batchSize, batchTimeoutMs }
 */
export function queueMemorySync(projectId, memoryId, memory, opts = {}) {
	const scheduler = getProjectScheduler(projectId);

	scheduler.queue.push({
		id: memoryId,
		memory,
		retries: 0,
		queuedAt: Date.now(),
	});

	scheduler.stats.queued += 1;

	// Check if batch is full or timer needs reset
	const batchSize = opts.batchSize || 50;
	const batchTimeoutMs = opts.batchTimeoutMs || 5000;

	if (scheduler.queue.length >= batchSize) {
		// Batch is full; flush immediately
		clearTimeout(scheduler.batchTimer);
		scheduler.batchTimer = null;
		scheduleBatchFlush(projectId, 0); // Immediate
	} else if (!scheduler.batchTimer) {
		// First item in batch; set timer
		scheduler.batchTimer = setTimeout(() => {
			scheduler.batchTimer = null;
			scheduleBatchFlush(projectId, 0);
		}, batchTimeoutMs);
	}
}

/**
 * Flush all queued memories for a project.
 * Called at session end or on demand.
 *
 * @param {string} projectId - Project identifier
 * @returns {Promise<object>} - { synced, failed, reason? }
 */
export async function flushSyncQueue(projectId) {
	const scheduler = getProjectScheduler(projectId);

	if (scheduler.flushing) {
		// Already flushing; queue will be processed
		return { synced: 0, failed: 0, reason: "already_flushing" };
	}

	if (scheduler.queue.length === 0) {
		return { synced: 0, failed: 0 };
	}

	scheduler.flushing = true;

	try {
		clearTimeout(scheduler.batchTimer);
		scheduler.batchTimer = null;

		const items = [...scheduler.queue];
		scheduler.queue = [];

		let synced = 0;
		let failed = 0;

		for (const item of items) {
			const success = await trySyncWithRetry(item, 0);
			if (success) {
				synced += 1;
				scheduler.stats.synced += 1;
			} else {
				failed += 1;
				scheduler.stats.failed += 1;
			}
		}

		return { synced, failed };
	} finally {
		scheduler.flushing = false;
	}
}

/**
 * Internal: schedule batch flush with optional delay.
 * Uses setImmediate to avoid blocking the event loop.
 */
function scheduleBatchFlush(projectId, delayMs = 0) {
	if (delayMs > 0) {
		setTimeout(() => performBatchFlush(projectId), delayMs);
	} else {
		setImmediate(() => performBatchFlush(projectId));
	}
}

/**
 * Internal: perform actual batch sync.
 * Batches all queued items into a single SM request.
 */
async function performBatchFlush(projectId) {
	const scheduler = getProjectScheduler(projectId);

	if (scheduler.queue.length === 0 || scheduler.flushing) {
		return;
	}

	scheduler.flushing = true;

	try {
		const items = [...scheduler.queue];
		scheduler.queue = [];

		// Batch sync: send all items in one request
		// For now, sync each individually; can be optimized later
		// (SM API may support bulk upsert)

		for (const item of items) {
			const success = await trySyncWithRetry(item, 0);
			if (success) {
				scheduler.stats.synced += 1;
			} else {
				scheduler.stats.failed += 1;
			}
		}
	} finally {
		scheduler.flushing = false;
	}
}

/**
 * Internal: try to sync a memory with exponential backoff.
 *
 * @param {object} item - { id, memory, retries }
 * @param {number} attempt - Current attempt number (0-indexed)
 * @returns {Promise<boolean>} - true if synced, false if failed after retries
 */
async function trySyncWithRetry(item, attempt = 0) {
	const MAX_RETRIES = 3;
	const BACKOFF_BASE_MS = 1000; // 1s

	try {
		// Fire sync to SM (non-blocking)
		syncMemoryToSm(item.id, item.memory);
		return true;
	} catch (err) {
		if (attempt < MAX_RETRIES) {
			// Exponential backoff: 1s, 2s, 4s
			const delayMs = BACKOFF_BASE_MS * 2 ** attempt;

			await delay(delayMs);

			return trySyncWithRetry(item, attempt + 1);
		}

		// Max retries exceeded; log warning
		if (typeof process !== "undefined" && process.env.DEBUG_SM_SYNC) {
			console.warn(
				`[SM sync] failed after ${MAX_RETRIES} retries: ${item.id}`,
				getErrorMessage(err),
			);
		}

		return false;
	}
}

/**
 * Get current sync status for a project.
 * Used by doctor checks or monitoring.
 *
 * @param {string} projectId - Project identifier
 * @returns {object} - { queued, synced, failed, flushing, schedulerActive }
 */
export function getSyncStatus(projectId) {
	const scheduler = getProjectScheduler(projectId);

	return {
		queued: scheduler.queue.length,
		synced: scheduler.stats.synced,
		failed: scheduler.stats.failed,
		flushing: scheduler.flushing,
		schedulerActive: scheduler.batchTimer !== null,
	};
}

/**
 * Clear all queued syncs for a project (for testing or manual reset).
 * Use with caution; memories in the queue will not be synced.
 *
 * @param {string} projectId - Project identifier
 */
export function clearSyncQueue(projectId) {
	const scheduler = getProjectScheduler(projectId);

	clearTimeout(scheduler.batchTimer);
	scheduler.batchTimer = null;
	scheduler.queue = [];
}

/**
 * Reset scheduler for a project (for testing).
 * Clears all state including stats.
 *
 * @param {string} projectId - Project identifier
 */
export function resetScheduler(projectId) {
	clearTimeout(globalState[projectId]?.batchTimer);
	delete globalState[projectId];
}

/**
 * Reset all schedulers (for testing).
 */
export function resetAllSchedulers() {
	for (const projectId of Object.keys(globalState)) {
		clearTimeout(globalState[projectId].batchTimer);
	}
	for (const key of Object.keys(globalState)) {
		delete globalState[key];
	}
}
