import { debugLog } from "../debug-logger.js";
import { getErrorMessage } from "../error-utils.js";
import { isValidGateType, UOK_GATE_TYPES } from "../engine-types.js";
import { getRelevantMemoriesRanked } from "../memory-store.js";
import {
	getDistinctGateIds,
	getGateCircuitBreaker,
	getGateRunStats,
	isDbAvailable,
	updateGateCircuitBreaker,
} from "../sf-db.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { validateGate } from "./contracts.js";
import { appendTraceEvent } from "./trace-writer.js";

const RETRY_MATRIX = {
	none: 0,
	policy: 0,
	input: 0,
	execution: 1,
	artifact: 1,
	verification: 1,
	closeout: 1,
	git: 1,
	timeout: 2,
	"manual-attention": 0,
	unknown: 0,
};

function envKeyForGate(gateId, suffix) {
	const normalized = gateId.replace(/-/g, "_").toUpperCase();
	return process.env[`SF_CIRCUIT_BREAKER_${normalized}_${suffix}`];
}

function resolveCircuitBreakerThresholds(gateId) {
	return {
		failureThreshold:
			Number(envKeyForGate(gateId, "THRESHOLD")) ||
			Number(process.env.SF_CIRCUIT_BREAKER_FAILURE_THRESHOLD) ||
			5,
		openDurationMs:
			Number(envKeyForGate(gateId, "OPEN_DURATION_MS")) ||
			Number(process.env.SF_CIRCUIT_BREAKER_OPEN_DURATION_MS) ||
			60_000,
		maxOpenDurationMs:
			Number(envKeyForGate(gateId, "MAX_OPEN_DURATION_MS")) ||
			Number(process.env.SF_CIRCUIT_BREAKER_MAX_OPEN_DURATION_MS) ||
			300_000,
		halfOpenMaxAttempts:
			Number(envKeyForGate(gateId, "HALF_OPEN_MAX_ATTEMPTS")) ||
			Number(process.env.SF_CIRCUIT_BREAKER_HALF_OPEN_MAX_ATTEMPTS) ||
			3,
	};
}

function computeCooldownMs(breaker, thresholds) {
	const streakAboveThreshold = Math.max(
		0,
		breaker.failureStreak - thresholds.failureThreshold,
	);
	return Math.min(
		thresholds.openDurationMs * 2 ** streakAboveThreshold,
		thresholds.maxOpenDurationMs,
	);
}

function formatCooldownMs(ms) {
	return ms >= 1000 ? `${Math.round(ms / 1000)}s` : `${ms}ms`;
}

function nowIso() {
	return new Date().toISOString();
}

/**
 * Build a gate_run trace event payload from a runner ctx + result.
 *
 * Carries the schema-v2 UOK run-context fields (surface, runControl,
 * permissionProfile, parentTrace) onto every gate_run event when the
 * caller supplies them. When absent, the fields are omitted entirely —
 * status-uok's classifier reads the event as "legacy" rather than
 * "incomplete", matching the pre-v2 behavior.
 *
 * Slice 3b of "Make UOK the SF Control Plane" — wires the autonomous
 * loop's gate emissions into the v2 control plane without forcing
 * every caller to know the column shape.
 */
function buildGateRunEvent(ctx, result, durationMs) {
	const event = {
		type: "gate_run",
		traceId: ctx.traceId,
		turnId: ctx.turnId,
		gateId: result.gateId,
		gateType: result.gateType,
		unitType: ctx.unitType,
		unitId: ctx.unitId,
		milestoneId: ctx.milestoneId,
		sliceId: ctx.sliceId,
		taskId: ctx.taskId,
		outcome: result.outcome,
		failureClass: result.failureClass,
		rationale: result.rationale,
		findings: result.findings,
		attempt: result.attempt,
		maxAttempts: result.maxAttempts,
		retryable: result.retryable,
		evaluatedAt: result.evaluatedAt,
		durationMs,
	};
	if (typeof ctx.surface === "string" && ctx.surface.length > 0) {
		event.surface = ctx.surface;
	}
	if (typeof ctx.runControl === "string" && ctx.runControl.length > 0) {
		event.runControl = ctx.runControl;
	}
	if (
		typeof ctx.permissionProfile === "string" &&
		ctx.permissionProfile.length > 0
	) {
		event.permissionProfile = ctx.permissionProfile;
	}
	if (typeof ctx.parentTrace === "string" && ctx.parentTrace.length > 0) {
		event.parentTrace = ctx.parentTrace;
	}
	return event;
}

/**
 * Enrich gate result with memory context for diagnostics.
 *
 * Purpose: Add historical pattern context to gate failures so operators
 * can understand if this is a known issue or new problem.
 *
 * Note: Does NOT change gate logic or decision, only adds metadata.
 */
export async function enrichGateResultWithMemory(gateResult, gateId) {
	if (!isDbAvailable() || gateResult.outcome === "pass") {
		return gateResult;
	}

	try {
		// Query: "Have we seen failures like this before?"
		const gateIdNorm = String(gateId || "unknown").toLowerCase();
		const failureClass = gateResult?.failureClass
			? ` ${gateResult.failureClass}`
			: "";
		const rationale = gateResult?.rationale ? ` ${gateResult.rationale}` : "";
		const query = `uok gate failure ${gateIdNorm}${failureClass}${rationale}`;
		const memories = await getRelevantMemoriesRanked(query, 2);

		if (memories.length > 0) {
			return {
				...gateResult,
				memoryContext: {
					hasHistoricalPattern: true,
					similarFailures: memories.map((m) => ({
						id: m.id,
						confidence: m.confidence,
						content: m.content.substring(0, 100), // First 100 chars
					})),
				},
			};
		}
	} catch (err) {
		// Degrade gracefully - memory enrichment never changes gate result
		debugLog("gate-memory-enrichment-failed", {
			gateId,
			error: getErrorMessage(err),
		});
	}

	return gateResult;
}

export class UokGateRunner {
	registry = new Map();
	register(gate) {
		const validation = validateGate(gate);
		if (!validation.valid) {
			throw new Error(`UokGateRunner.register: ${validation.reason}`);
		}
		if (!isValidGateType(gate.type)) {
			throw new TypeError(
				`UokGateRunner.register: gate "${gate.id}" has invalid type=${JSON.stringify(gate.type)}; must be one of: ${UOK_GATE_TYPES.join(", ")} (per ADR-0075)`,
			);
		}
		this.registry.set(gate.id, gate);
	}
	list() {
		return Array.from(this.registry.values());
	}

	getHealthSummary() {
		const gates = this.list();
		const ids =
			gates.length > 0
				? gates.map((g) => g.id)
				: getDistinctGateIds().length > 0
					? getDistinctGateIds()
					: [];
		return {
			gates: ids.map((id) => {
				const stats = getGateRunStats(id, 24);
				const cb = getGateCircuitBreaker(id);
				const registered = this.registry.get(id);
				return {
					id,
					type: registered?.type ?? "unknown",
					...stats,
					circuitBreaker: cb.state,
					failureStreak: cb.failureStreak,
				};
			}),
		};
	}

	_checkCircuitBreaker(gateId) {
		const thresholds = resolveCircuitBreakerThresholds(gateId);
		const breaker = getGateCircuitBreaker(gateId);
		if (breaker.state === "open") {
			const openedAt = breaker.openedAt ? Date.parse(breaker.openedAt) : 0;
			const cooldownMs = computeCooldownMs(breaker, thresholds);
			if (Date.now() - openedAt >= cooldownMs) {
				// Transition to half-open automatically after cooldown
				updateGateCircuitBreaker(gateId, {
					state: "half-open",
					failureStreak: breaker.failureStreak,
					halfOpenAttempts: 0,
				});
				return { blocked: false };
			}
			return {
				blocked: true,
				reason: `Circuit breaker OPEN for ${gateId} (failure streak ${breaker.failureStreak}, cooldown ${formatCooldownMs(cooldownMs)}). Cooldown until ${new Date(openedAt + cooldownMs).toISOString()}.`,
			};
		}
		if (breaker.state === "half-open") {
			if (breaker.halfOpenAttempts >= thresholds.halfOpenMaxAttempts) {
				// Too many half-open attempts without success — go back to open
				updateGateCircuitBreaker(gateId, {
					state: "open",
					failureStreak: breaker.failureStreak,
					openedAt: nowIso(),
					halfOpenAttempts: 0,
				});
				return {
					blocked: true,
					reason: `Circuit breaker half-open limit exceeded for ${gateId}; reopening.`,
				};
			}
		}
		return { blocked: false };
	}

	_updateCircuitBreaker(gateId, succeeded) {
		const breaker = getGateCircuitBreaker(gateId);
		if (succeeded) {
			updateGateCircuitBreaker(gateId, {
				state: "closed",
				failureStreak: 0,
				lastFailureAt: null,
				openedAt: null,
				halfOpenAttempts: 0,
			});
			return;
		}
		const nextStreak = breaker.failureStreak + 1;
		if (breaker.state === "half-open") {
			updateGateCircuitBreaker(gateId, {
				state: "open",
				failureStreak: nextStreak,
				lastFailureAt: nowIso(),
				openedAt: nowIso(),
				halfOpenAttempts: 0,
			});
			return;
		}
		const { failureThreshold } = resolveCircuitBreakerThresholds(gateId);
		if (nextStreak >= failureThreshold) {
			updateGateCircuitBreaker(gateId, {
				state: "open",
				failureStreak: nextStreak,
				lastFailureAt: nowIso(),
				openedAt: nowIso(),
				halfOpenAttempts: 0,
			});
			return;
		}
		updateGateCircuitBreaker(gateId, {
			state: "closed",
			failureStreak: nextStreak,
			lastFailureAt: nowIso(),
			halfOpenAttempts: 0,
		});
	}

	async run(id, ctx) {
		const gate = this.registry.get(id);
		if (!gate) {
			const now = nowIso();
			const unknownResult = {
				gateId: id,
				gateType: "unknown",
				outcome: "manual-attention",
				failureClass: "unknown",
				rationale: `Gate ${id} not registered`,
				attempt: 1,
				maxAttempts: 1,
				retryable: false,
				evaluatedAt: now,
			};
			emitUokAuditEvent(
				ctx.basePath,
				buildAuditEnvelope({
					traceId: ctx.traceId,
					turnId: ctx.turnId,
					category: "gate",
					type: "gate-run",
					payload: {
						gateId: unknownResult.gateId,
						gateType: unknownResult.gateType,
						outcome: unknownResult.outcome,
						failureClass: unknownResult.failureClass,
						attempt: unknownResult.attempt,
						maxAttempts: unknownResult.maxAttempts,
						retryable: unknownResult.retryable,
					},
				}),
			);
			appendTraceEvent(
				ctx.basePath,
				ctx.traceId,
				buildGateRunEvent(ctx, unknownResult, 0),
			);
			return unknownResult;
		}

		// Circuit breaker check
		const cbCheck = this._checkCircuitBreaker(id);
		if (cbCheck.blocked) {
			const now = nowIso();
			const cbResult = {
				gateId: id,
				gateType: gate.type,
				outcome: "fail",
				failureClass: "policy",
				rationale: cbCheck.reason,
				attempt: 1,
				maxAttempts: 1,
				retryable: false,
				evaluatedAt: now,
			};
			emitUokAuditEvent(
				ctx.basePath,
				buildAuditEnvelope({
					traceId: ctx.traceId,
					turnId: ctx.turnId,
					category: "gate",
					type: "gate-run",
					payload: {
						gateId: cbResult.gateId,
						gateType: cbResult.gateType,
						outcome: cbResult.outcome,
						failureClass: cbResult.failureClass,
						attempt: cbResult.attempt,
						maxAttempts: cbResult.maxAttempts,
						retryable: cbResult.retryable,
					},
				}),
			);
			appendTraceEvent(
				ctx.basePath,
				ctx.traceId,
				buildGateRunEvent(ctx, cbResult, 0),
			);
			return cbResult;
		}

		// Half-open tracking
		const breaker = getGateCircuitBreaker(id);
		if (breaker.state === "half-open") {
			updateGateCircuitBreaker(id, {
				state: "half-open",
				failureStreak: breaker.failureStreak,
				halfOpenAttempts: breaker.halfOpenAttempts + 1,
			});
		}

		let attempt = 0;
		let final = null;
		const maxAttemptsByFailureClass = RETRY_MATRIX;
		const maxAttemptsCeiling = Math.max(...Object.values(RETRY_MATRIX)) + 1;
		while (attempt < maxAttemptsCeiling) {
			attempt += 1;
			const now = nowIso();
			const startedAt = Date.now();
			let result;
			try {
				result = await gate.execute(ctx, attempt);
			} catch (err) {
				const message = getErrorMessage(err);
				result = {
					outcome: "fail",
					failureClass: "unknown",
					rationale: message,
				};
			}
			const durationMs = Date.now() - startedAt;
			const failureClass =
				result.failureClass ?? (result.outcome === "pass" ? "none" : "unknown");
			const retryBudget = maxAttemptsByFailureClass[failureClass] ?? 0;
			const retryable = result.outcome !== "pass" && attempt <= retryBudget;
			final = {
				gateId: gate.id,
				gateType: gate.type,
				outcome: retryable ? "retry" : result.outcome,
				failureClass,
				rationale: result.rationale,
				findings: result.findings,
				attempt,
				maxAttempts: retryBudget + 1,
				retryable,
				evaluatedAt: now,
			};
			emitUokAuditEvent(
				ctx.basePath,
				buildAuditEnvelope({
					traceId: ctx.traceId,
					turnId: ctx.turnId,
					category: "gate",
					type: "gate-run",
					payload: {
						gateId: final.gateId,
						gateType: final.gateType,
						outcome: final.outcome,
						failureClass: final.failureClass,
						attempt: final.attempt,
						maxAttempts: final.maxAttempts,
						retryable: final.retryable,
						durationMs,
					},
				}),
			);
			appendTraceEvent(
				ctx.basePath,
				ctx.traceId,
				buildGateRunEvent(ctx, final, durationMs),
			);
			if (!retryable) break;
		}

		// Update circuit breaker based on final outcome
		const succeeded = final?.outcome === "pass";
		this._updateCircuitBreaker(id, succeeded);
		if (final && !succeeded && breaker.state === "half-open") {
			const reopened = getGateCircuitBreaker(id);
			const cooldownMs = computeCooldownMs(
				reopened,
				resolveCircuitBreakerThresholds(id),
			);
			final = {
				...final,
				rationale: `${final.rationale}; Circuit breaker reopened for ${id} (failure streak ${reopened.failureStreak}, cooldown ${formatCooldownMs(cooldownMs)}).`,
			};
		}

		const result = final ?? {
			gateId: gate.id,
			gateType: gate.type,
			outcome: "manual-attention",
			failureClass: "unknown",
			attempt: 1,
			maxAttempts: 1,
			retryable: false,
			evaluatedAt: nowIso(),
		};
		if (result.outcome !== "pass") {
			return await enrichGateResultWithMemory(result, id);
		}
		return result;
	}
}
