/**
 * UOK Outcome Learning Gate
 *
 * Purpose: query the local LLM task outcome ledger to detect failure patterns
 * (model-specific, unit-type-specific, provider-specific) and autonomously
 * recommend model or strategy changes before dispatching the next unit.
 *
 * Consumer: UOK gate-runner during post-unit verification and pre-dispatch
 * advisory.
 */

import { compareModelCost } from "../model-cost-table.js";
import {
	getLlmTaskOutcomeStats,
	getLlmTaskOutcomesByUnit,
	getRecentLlmTaskOutcomes,
} from "../sf-db.js";

const DEFAULT_MIN_SAMPLE_SIZE = 3;
const DEFAULT_FAILURE_RATE_THRESHOLD = 0.6;
const DEFAULT_LOOKBACK_HOURS = 168; // 1 week

function computeFailureRate(outcomes) {
	if (outcomes.length === 0) return 0;
	const failed = outcomes.filter((o) => !o.succeeded).length;
	return failed / outcomes.length;
}

function groupByModel(outcomes) {
	const groups = new Map();
	for (const o of outcomes) {
		const list = groups.get(o.model_id) ?? [];
		list.push(o);
		groups.set(o.model_id, list);
	}
	return groups;
}

function groupByUnitType(outcomes) {
	const groups = new Map();
	for (const o of outcomes) {
		const list = groups.get(o.unit_type) ?? [];
		list.push(o);
		groups.set(o.unit_type, list);
	}
	return groups;
}

export class OutcomeLearningGate {
	constructor(options = {}) {
		this.id = "outcome-learning";
		this.type = "learning";
		this.minSampleSize = options.minSampleSize ?? DEFAULT_MIN_SAMPLE_SIZE;
		this.failureRateThreshold =
			options.failureRateThreshold ?? DEFAULT_FAILURE_RATE_THRESHOLD;
		this.lookbackHours = options.lookbackHours ?? DEFAULT_LOOKBACK_HOURS;
	}

	/**
	 * @param {import("./contracts.js").UokContext} ctx
	 * @param {number} _attempt
	 */
	async execute(ctx, _attempt) {
		const unitType = ctx.unitType ?? "unknown";
		const unitId = ctx.unitId ?? "unknown";
		const modelId = ctx.modelId ?? "unknown";

		const findings = [];
		const recommendations = [];

		// 1. Unit-specific pattern
		const unitOutcomes = getLlmTaskOutcomesByUnit(unitType, unitId, 20);
		if (unitOutcomes.length >= this.minSampleSize) {
			const unitRate = computeFailureRate(unitOutcomes);
			if (unitRate >= this.failureRateThreshold) {
				findings.push(
					`Unit ${unitId} failure rate ${(unitRate * 100).toFixed(0)}% over ${unitOutcomes.length} attempt(s).`,
				);
				recommendations.push(
					"Consider splitting this unit into smaller tasks or changing approach.",
				);
			}
		}

		// 2. Model-specific pattern (rolling window)
		const modelStats = getLlmTaskOutcomeStats(modelId, 24);
		if (modelStats.total >= this.minSampleSize) {
			const modelRate = modelStats.failed / modelStats.total;
			if (modelRate >= this.failureRateThreshold) {
				findings.push(
					`Model ${modelId} failure rate ${(modelRate * 100).toFixed(0)}% over ${modelStats.total} attempt(s) in 24h.`,
				);
				const alternatives = this._findBetterModels(modelId);
				if (alternatives.length > 0) {
					recommendations.push(
						`Switch to a more reliable model: ${alternatives.slice(0, 3).join(", ")}.`,
					);
				} else {
					recommendations.push(
						"Model shows poor reliability; consider provider change or human review.",
					);
				}
			}
		}

		// 3. System-wide pattern detection (lookback window)
		const recent = getRecentLlmTaskOutcomes(this.lookbackHours, 200);
		if (recent.length >= this.minSampleSize) {
			const byUnitType = groupByUnitType(recent);
			for (const [ut, outcomes] of byUnitType) {
				if (outcomes.length < this.minSampleSize) continue;
				const rate = computeFailureRate(outcomes);
				if (rate >= this.failureRateThreshold) {
					findings.push(
						`Unit type '${ut}' failure rate ${(rate * 100).toFixed(0)}% over ${outcomes.length} attempt(s) in last ${this.lookbackHours}h.`,
					);
				}
			}

			const byModel = groupByModel(recent);
			for (const [mid, outcomes] of byModel) {
				if (outcomes.length < this.minSampleSize) continue;
				const rate = computeFailureRate(outcomes);
				if (rate >= this.failureRateThreshold) {
					findings.push(
						`Model '${mid}' failure rate ${(rate * 100).toFixed(0)}% over ${outcomes.length} attempt(s) in last ${this.lookbackHours}h.`,
					);
				}
			}
		}

		// 4. Escalation / blocker pattern
		const escalatedCount = recent.filter((o) => o.escalated).length;
		if (escalatedCount >= 3) {
			findings.push(
				`${escalatedCount} escalated task(s) in last ${this.lookbackHours}h — systemic blocker risk.`,
			);
			recommendations.push(
				"Review milestone plans for hidden dependencies or scope creep.",
			);
		}

		if (findings.length > 0) {
			return {
				outcome: "fail",
				failureClass: "policy",
				rationale: `Outcome Learning detected ${findings.length} pattern(s) suggesting degraded success probability.`,
				findings: findings.join("; "),
				recommendation:
					recommendations.join(" ") ||
					"Review recent failures before continuing.",
			};
		}

		return {
			outcome: "pass",
			rationale: `No adverse patterns detected. Unit ${unitOutcomes.length} sample(s), model ${modelStats.total} sample(s), system ${recent.length} sample(s).`,
		};
	}

	_findBetterModels(currentModelId) {
		// Simple heuristic: find models that are cheaper (often correlated with
		// simpler/faster and sometimes more reliable for narrow tasks).
		// In a fuller implementation this would query outcome stats per model.
		const candidates = [
			"claude-haiku-4-5",
			"gemini-2.0-flash",
			"gpt-4o-mini",
			"gpt-4.1-mini",
			"deepseek-chat",
		];
		return candidates
			.filter((c) => compareModelCost(c, currentModelId) < 0)
			.slice(0, 3);
	}
}
