//#region src/cognitive/feedback/preference-learner.ts
/**
* Thompson Sampling preference learner.
*
* Each topic has a Beta(α, β) posterior. On positive feedback α+=1,
* on negative feedback β+=1. Sample from posterior to decide whether
* to push this topic.
*
* Cold start: optimistic initialization with α=2, β=1 (prior belief of relevance).
*/
const OPTIMISTIC_ALPHA = 2;
const OPTIMISTIC_BETA = 1;
/** 90-day half-life for preference decay toward priors */
const DECAY_HALF_LIFE_MS = 2160 * 60 * 60 * 1e3;
/**
* Exponential decay of a single bandit toward priors.
* Formula: decayed = prior + (current - prior) * exp(-ln2 * age / halfLife)
* Clamped so alpha/beta never drop below priors.
* Legacy bandits without lastUpdated are treated as if updated one half-life ago.
*/
function decayBandit(bandit, nowMs, halfLifeMs = DECAY_HALF_LIFE_MS) {
	const age = nowMs - (bandit.lastUpdated ?? nowMs - halfLifeMs);
	if (age <= 0) return bandit;
	const factor = Math.exp(-Math.LN2 * age / halfLifeMs);
	const decayedAlpha = OPTIMISTIC_ALPHA + (bandit.alpha - OPTIMISTIC_ALPHA) * factor;
	const decayedBeta = OPTIMISTIC_BETA + (bandit.beta - OPTIMISTIC_BETA) * factor;
	return {
		alpha: Math.max(OPTIMISTIC_ALPHA, decayedAlpha),
		beta: Math.max(OPTIMISTIC_BETA, decayedBeta),
		lastUpdated: bandit.lastUpdated ?? nowMs - halfLifeMs
	};
}
function decayAllBandits(profile, nowMs) {
	const newBandits = {};
	for (const [topic, bandit] of Object.entries(profile.topicBandits)) newBandits[topic] = decayBandit(bandit, nowMs);
	return {
		...profile,
		topicBandits: newBandits
	};
}
/**
* Update the bandit for a topic based on feedback.
* Returns a new FeedbackProfile (does not mutate input).
*/
function updateBanditFromFeedback(profile, feedback) {
	const topic = feedback.topic ?? "general";
	const bandit = decayBandit(profile.topicBandits[topic] ?? {
		alpha: OPTIMISTIC_ALPHA,
		beta: OPTIMISTIC_BETA
	}, feedback.timestamp);
	let newAlpha = bandit.alpha;
	let newBeta = bandit.beta;
	switch (feedback.type) {
		case "positive":
		case "engaged":
			newAlpha += 1;
			break;
		case "negative":
			newBeta += 1;
			break;
		case "neutral":
			newBeta += .5;
			break;
	}
	return {
		...profile,
		topicBandits: {
			...profile.topicBandits,
			[topic]: {
				alpha: newAlpha,
				beta: newBeta,
				lastUpdated: feedback.timestamp
			}
		}
	};
}
/**
* Sample from all topic bandits to get exploration-exploitation scores.
* Higher score = more likely to be relevant NOW.
*/
function sampleTopicScores(profile, rng) {
	const random = rng ?? Math.random;
	const scores = /* @__PURE__ */ new Map();
	for (const [topic, bandit] of Object.entries(profile.topicBandits)) scores.set(topic, sampleBeta(bandit.alpha, bandit.beta, random));
	return scores;
}
/**
* Pick the best topic to push based on Thompson Sampling scores.
* Returns undefined if no topics available or if all scores are below threshold.
*/
function pickBestTopic(profile, options) {
	const minScore = options?.minScore ?? .3;
	const exclude = new Set(options?.excludeTopics ?? []);
	const scores = sampleTopicScores(profile, options?.rng);
	let bestTopic;
	let bestScore = -1;
	for (const [topic, score] of scores) {
		if (exclude.has(topic)) continue;
		if (score < minScore) continue;
		if (score > bestScore) {
			bestScore = score;
			bestTopic = topic;
		}
	}
	return bestTopic;
}
/**
* Update the optimal frequency based on feedback patterns.
* If user responds positively, increase frequency. If negatively, decrease.
*/
function adaptFrequency(currentHours, feedback) {
	const delta = feedback.type === "positive" || feedback.type === "engaged" ? -.5 : feedback.type === "negative" ? 2 : 0;
	return Math.max(1, Math.min(48, currentHours + delta));
}
/**
* Pick the best prompt variant by Thompson Sampling over prompt bandits.
* For each armKey, get or create a TopicBandit from profile.promptBandits
* (cold start: optimistic prior { alpha: 2, beta: 1 }).
* Returns the 0-based index of the arm with the highest sampled score.
*/
function pickPromptVariant(profile, armKeys, rng) {
	const random = rng ?? Math.random;
	const bandits = profile.promptBandits ?? {};
	let bestIndex = 0;
	let bestScore = -1;
	for (let i = 0; i < armKeys.length; i++) {
		const bandit = bandits[armKeys[i]] ?? {
			alpha: OPTIMISTIC_ALPHA,
			beta: OPTIMISTIC_BETA
		};
		const score = sampleBeta(bandit.alpha, bandit.beta, random);
		if (score > bestScore) {
			bestScore = score;
			bestIndex = i;
		}
	}
	return bestIndex;
}
function sampleBeta(alpha, beta, rng) {
	const x = sampleGamma(alpha, rng);
	return x / (x + sampleGamma(beta, rng));
}
function sampleGamma(shape, rng) {
	if (shape < 1) return sampleGamma(shape + 1, rng) * Math.pow(rng(), 1 / shape);
	const d = shape - 1 / 3;
	const c = 1 / Math.sqrt(9 * d);
	while (true) {
		let x;
		let v;
		do {
			x = randomNormal(rng);
			v = 1 + c * x;
		} while (v <= 0);
		v = v * v * v;
		const u = rng();
		if (u < 1 - .0331 * x * x * x * x) return d * v;
		if (Math.log(u) < .5 * x * x + d * (1 - v + Math.log(v))) return d * v;
	}
}
function randomNormal(rng) {
	const u1 = rng();
	const u2 = rng();
	return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
//#endregion
export { pickPromptVariant as a, pickBestTopic as i, decayAllBandits as n, updateBanditFromFeedback as o, decayBandit as r, adaptFrequency as t };
