// SF Extension — Routing History (Adaptive Learning)
// Tracks success/failure per tier per unit-type pattern to improve
// classification accuracy over time.
import {
	clearRoutingHistory as dbClearRoutingHistory,
	getAllRoutingHistory,
	getDatabase,
	insertRoutingFeedback,
	upsertRoutingOutcome,
} from "./sf-db.js";

// ─── Constants ───────────────────────────────────────────────────────────────
const ROLLING_WINDOW = 50; // only consider last N entries per pattern
const FAILURE_THRESHOLD = 0.2; // >20% failure rate triggers tier bump
const FEEDBACK_WEIGHT = 2; // feedback signals count 2x vs automatic
// ─── In-Memory State ─────────────────────────────────────────────────────────
let history = null;
// Latches to false when the `routing_history` table is observed missing at
// init time. Subsequent DB writes from recordOutcome/recordFeedback are then
// skipped so a stale-schema project doesn't repeatedly throw on every
// dispatch. The next openDatabase that successfully runs the v53 migration
// will let a later initRoutingHistory call flip this back to true.
let _dbTableAvailable = true;
// ─── Public API ──────────────────────────────────────────────────────────────
/**
 * Initialize routing history for a project.
 *
 * Resilient to a missing `routing_history` table: a project DB whose schema
 * version predates the routing_history migration (v53) will throw `no such
 * table: routing_history` on the underlying SELECT. We swallow that one
 * specific case so auto-start is not blocked by a stale schema; the
 * in-memory history simply starts empty and accumulates from scratch.
 * Anything else (corrupt DB, permission errors) re-throws so it remains
 * visible.
 */
export function initRoutingHistory(_base) {
	history = createEmptyHistory();
	const db = getDatabase();
	if (!db) return;
	let rows;
	try {
		rows = getAllRoutingHistory(db);
		_dbTableAvailable = true;
	} catch (err) {
		const message = err?.message ? String(err.message) : "";
		if (/no such table:\s*routing_history/i.test(message)) {
			// Schema lags the code — fresh project, or a project whose DB never
			// migrated past v52. Start with empty in-memory state and latch the
			// flag so recordOutcome/recordFeedback skip their DB writes for the
			// remainder of the session instead of crashing on every dispatch.
			rows = [];
			_dbTableAvailable = false;
		} else {
			throw err;
		}
	}
	for (const row of rows) {
		if (!history.patterns[row.pattern]) {
			history.patterns[row.pattern] = {
				light: { success: 0, fail: 0 },
				standard: { success: 0, fail: 0 },
				heavy: { success: 0, fail: 0 },
			};
		}
		if (
			row.tier === "light" ||
			row.tier === "standard" ||
			row.tier === "heavy"
		) {
			history.patterns[row.pattern][row.tier].success = row.success_count;
			history.patterns[row.pattern][row.tier].fail = row.fail_count;
		}
		if (row.updated_at > history.updatedAt) {
			history.updatedAt = row.updated_at;
		}
	}
}
/**
 * Reset routing history state.
 */
export function resetRoutingHistory() {
	history = null;
}
/**
 * Record the outcome of a unit dispatch.
 *
 * @param unitType  The unit type (e.g. "execute-task")
 * @param tier      The tier that was used
 * @param success   Whether the unit completed successfully
 * @param tags      Optional tags from task metadata (e.g. ["docs", "test"])
 */
export function recordOutcome(unitType, tier, success, tags) {
	if (!history) return;
	const db = getDatabase();
	const canWriteDb = db && _dbTableAvailable;
	// Record for the base unit type
	const basePattern = unitType;
	ensurePattern(basePattern);
	const outcome = history.patterns[basePattern][tier];
	if (success) outcome.success++;
	else outcome.fail++;
	if (canWriteDb) upsertRoutingOutcome(db, basePattern, tier, success);
	// Record for tag-specific patterns (e.g. "execute-task:docs")
	if (tags && tags.length > 0) {
		for (const tag of tags) {
			const tagPattern = `${unitType}:${tag}`;
			ensurePattern(tagPattern);
			const tagOutcome = history.patterns[tagPattern][tier];
			if (success) tagOutcome.success++;
			else tagOutcome.fail++;
			if (canWriteDb) upsertRoutingOutcome(db, tagPattern, tier, success);
		}
	}
	// Apply rolling window — cap total entries per tier per pattern
	for (const pattern of Object.keys(history.patterns)) {
		const p = history.patterns[pattern];
		for (const t of ["light", "standard", "heavy"]) {
			const total = p[t].success + p[t].fail;
			if (total > ROLLING_WINDOW) {
				const scale = ROLLING_WINDOW / total;
				p[t].success = Math.round(p[t].success * scale);
				p[t].fail = Math.round(p[t].fail * scale);
			}
		}
	}
	history.updatedAt = new Date().toISOString();
}
/**
 * Record user feedback for the last completed unit.
 */
export function recordFeedback(unitType, _unitId, tier, rating) {
	if (!history) return;
	const ts = new Date().toISOString();
	history.feedback.push({
		pattern: unitType,
		tier,
		feedback: rating,
		ts,
	});
	// Cap feedback array at 200 entries
	if (history.feedback.length > 200) {
		history.feedback = history.feedback.slice(-200);
	}
	const db = getDatabase();
	const canWriteDb = db && _dbTableAvailable;
	if (canWriteDb) insertRoutingFeedback(db, unitType, tier, rating);
	// Apply feedback as weighted outcome
	const pattern = unitType;
	ensurePattern(pattern);
	if (rating === "over") {
		// User says this could have used a simpler model → record as success at current tier
		// and also as success at one tier lower (encourages more downgrading)
		const lower = tierBelow(tier);
		if (lower) {
			const outcomes = history.patterns[pattern][lower];
			outcomes.success += FEEDBACK_WEIGHT;
			if (canWriteDb) {
				for (let i = 0; i < FEEDBACK_WEIGHT; i++) {
					upsertRoutingOutcome(db, pattern, lower, true);
				}
			}
		}
	} else if (rating === "under") {
		// User says this needed a better model → record as failure at current tier
		const outcomes = history.patterns[pattern][tier];
		outcomes.fail += FEEDBACK_WEIGHT;
		if (canWriteDb) {
			for (let i = 0; i < FEEDBACK_WEIGHT; i++) {
				upsertRoutingOutcome(db, pattern, tier, false);
			}
		}
	}
	// "ok" = no adjustment needed
	history.updatedAt = new Date().toISOString();
}
/**
 * Get the recommended tier adjustment for a given pattern.
 * Returns the tier to bump to if the failure rate exceeds threshold,
 * or null if no adjustment is needed.
 */
export function getAdaptiveTierAdjustment(unitType, currentTier, tags) {
	if (!history) return null;
	// Check tag-specific patterns first (more specific)
	if (tags && tags.length > 0) {
		for (const tag of tags) {
			const tagPattern = `${unitType}:${tag}`;
			const adjustment = checkPatternFailureRate(tagPattern, currentTier);
			if (adjustment) return adjustment;
		}
	}
	// Fall back to base pattern
	return checkPatternFailureRate(unitType, currentTier);
}
/**
 * Clear all routing history (user-triggered reset).
 */
export function clearRoutingHistory(_base) {
	history = createEmptyHistory();
	const db = getDatabase();
	if (db && _dbTableAvailable) dbClearRoutingHistory(db);
}
/**
 * Get current history data (for display/debugging).
 */
export function getRoutingHistory() {
	return history;
}
// ─── Internal ────────────────────────────────────────────────────────────────
function checkPatternFailureRate(pattern, tier) {
	if (!history?.patterns[pattern]) return null;
	const outcomes = history.patterns[pattern][tier];
	const total = outcomes.success + outcomes.fail;
	if (total < 3) return null; // Not enough data
	const failureRate = outcomes.fail / total;
	if (failureRate > FAILURE_THRESHOLD) {
		// Bump to next tier
		return tierAbove(tier);
	}
	return null;
}
function tierAbove(tier) {
	switch (tier) {
		case "light":
			return "standard";
		case "standard":
			return "heavy";
		case "heavy":
			return null;
	}
}
function tierBelow(tier) {
	switch (tier) {
		case "light":
			return null;
		case "standard":
			return "light";
		case "heavy":
			return "standard";
	}
}
function ensurePattern(pattern) {
	if (!history) return;
	if (!history.patterns[pattern]) {
		history.patterns[pattern] = {
			light: { success: 0, fail: 0 },
			standard: { success: 0, fail: 0 },
			heavy: { success: 0, fail: 0 },
		};
	}
}
function createEmptyHistory() {
	return {
		version: 1,
		patterns: {},
		feedback: [],
		updatedAt: new Date().toISOString(),
	};
}
