import { dirname } from "node:path";
import { SF_STALE_STATE, SFError } from "../errors.js";
import { getGateIdsForTurn } from "../gate-registry.js";
import { uokRunContextToGateColumns } from "../uok/run-context.js";
import { readTraceEvents } from "../uok/trace-writer.js";
import { logWarning } from "../workflow-logger.js";
import {
	_getAdapter,
	getDbPath,
	rowToGate,
	transaction,
} from "./sf-db-core.js";

export function insertGateRow(g) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	// Callers pass canonical camelCase uokContext (the shape returned by
	// buildUokRunContext). This module owns the snake_case translation so
	// callers don't need to know the column names. uokRunContextToGateColumns
	// returns null when the context is invalid/incomplete, which leaves the
	// columns NULL — same shape as pre-v66 (legacy) rows, so the classifier
	// will mark the row "legacy" or "incomplete" rather than silently passing.
	const uokCols = uokRunContextToGateColumns(g.uokContext) ?? null;
	currentDb
		.prepare(`INSERT OR IGNORE INTO quality_gates (
       milestone_id, slice_id, gate_id, scope, task_id, status,
       surface, run_control, permission_profile, trace_id, parent_trace
     )
     VALUES (:mid, :sid, :gid, :scope, :tid, :status,
             :surface, :run_control, :permission_profile, :trace_id, :parent_trace)`)
		.run({
			":mid": g.milestoneId,
			":sid": g.sliceId,
			":gid": g.gateId,
			":scope": g.scope,
			":tid": g.taskId ?? "",
			":status": g.status ?? "pending",
			":surface": uokCols?.surface ?? null,
			":run_control": uokCols?.run_control ?? null,
			":permission_profile": uokCols?.permission_profile ?? null,
			":trace_id": uokCols?.trace_id ?? null,
			":parent_trace": uokCols?.parent_trace ?? null,
		});
}

export function saveGateResult(g) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	// Schema-v2 UOK run context is updated only when the caller supplies a valid
	// canonical context. Existing rows keep their context (or stay legacy/
	// null) when no context is provided. COALESCE semantics on each column
	// mean "only overwrite when non-null" — so a legacy row stays legacy
	// unless this update brings a full schema-v2 ctx.
	const uokCols = uokRunContextToGateColumns(g.uokContext) ?? null;
	currentDb
		.prepare(`UPDATE quality_gates
     SET status = 'complete', verdict = :verdict, rationale = :rationale,
         findings = :findings, evaluated_at = :evaluated_at,
         surface = COALESCE(:surface, surface),
         run_control = COALESCE(:run_control, run_control),
         permission_profile = COALESCE(:permission_profile, permission_profile),
         trace_id = COALESCE(:trace_id, trace_id),
         parent_trace = COALESCE(:parent_trace, parent_trace)
     WHERE milestone_id = :mid AND slice_id = :sid AND gate_id = :gid
       AND task_id = :tid`)
		.run({
			":mid": g.milestoneId,
			":sid": g.sliceId,
			":gid": g.gateId,
			":tid": g.taskId ?? "",
			":verdict": g.verdict,
			":rationale": g.rationale,
			":findings": g.findings,
			":evaluated_at": new Date().toISOString(),
			":surface": uokCols?.surface ?? null,
			":run_control": uokCols?.run_control ?? null,
			":permission_profile": uokCols?.permission_profile ?? null,
			":trace_id": uokCols?.trace_id ?? null,
			":parent_trace": uokCols?.parent_trace ?? null,
		});
	const outcome =
		g.verdict === "pass"
			? "pass"
			: g.verdict === "omitted"
				? "manual-attention"
				: "fail";
	insertGateRun({
		traceId: `quality-gate:${g.milestoneId}:${g.sliceId}`,
		turnId: `gate:${g.gateId}:${g.taskId ?? "slice"}`,
		gateId: g.gateId,
		gateType: "quality-gate",
		milestoneId: g.milestoneId,
		sliceId: g.sliceId,
		taskId: g.taskId ?? undefined,
		outcome,
		failureClass:
			outcome === "fail"
				? "verification"
				: outcome === "manual-attention"
					? "manual-attention"
					: "none",
		rationale: g.rationale,
		findings: g.findings,
		attempt: 1,
		maxAttempts: 1,
		retryable: false,
		evaluatedAt: new Date().toISOString(),
	});
}

export function getPendingGates(milestoneId, sliceId, scope) {
	const currentDb = _getAdapter();
	if (!currentDb) return [];
	const sql = scope
		? `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid AND scope = :scope AND status = 'pending'`
		: `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid AND status = 'pending'`;
	const params = {
		":mid": milestoneId,
		":sid": sliceId,
	};
	if (scope) params[":scope"] = scope;
	return currentDb.prepare(sql).all(params).map(rowToGate);
}

export function getGateResults(milestoneId, sliceId, scope) {
	const currentDb = _getAdapter();
	if (!currentDb) return [];
	const sql = scope
		? `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid AND scope = :scope`
		: `SELECT * FROM quality_gates WHERE milestone_id = :mid AND slice_id = :sid`;
	const params = {
		":mid": milestoneId,
		":sid": sliceId,
	};
	if (scope) params[":scope"] = scope;
	return currentDb.prepare(sql).all(params).map(rowToGate);
}

export function markAllGatesOmitted(milestoneId, sliceId) {
	const currentDb = _getAdapter();
	if (!currentDb) return;
	currentDb
		.prepare(`UPDATE quality_gates SET status = 'omitted', verdict = 'omitted', evaluated_at = :now
     WHERE milestone_id = :mid AND slice_id = :sid AND status = 'pending'`)
		.run({
			":mid": milestoneId,
			":sid": sliceId,
			":now": new Date().toISOString(),
		});
}

export function getPendingSliceGateCount(milestoneId, sliceId) {
	const currentDb = _getAdapter();
	if (!currentDb) return 0;
	const row = currentDb
		.prepare(`SELECT COUNT(*) as cnt FROM quality_gates
     WHERE milestone_id = :mid AND slice_id = :sid AND scope = 'slice' AND status = 'pending'`)
		.get({ ":mid": milestoneId, ":sid": sliceId });
	return row ? row["cnt"] : 0;
}

export function getPendingGatesForTurn(milestoneId, sliceId, turn, taskId) {
	const currentDb = _getAdapter();
	if (!currentDb) return [];
	const ids = getGateIdsForTurn(turn);
	if (ids.size === 0) return [];
	const idList = [...ids];
	const placeholders = idList.map((_, i) => `:gid${i}`).join(",");
	const params = {
		":mid": milestoneId,
		":sid": sliceId,
	};
	idList.forEach((id, i) => {
		params[`:gid${i}`] = id;
	});
	let sql = `SELECT * FROM quality_gates
     WHERE milestone_id = :mid AND slice_id = :sid
       AND status = 'pending'
       AND gate_id IN (${placeholders})`;
	if (taskId !== undefined) {
		sql += ` AND task_id = :tid`;
		params[":tid"] = taskId;
	}
	return currentDb.prepare(sql).all(params).map(rowToGate);
}

export function getPendingGateCountForTurn(milestoneId, sliceId, turn) {
	return getPendingGatesForTurn(milestoneId, sliceId, turn).length;
}

export function insertGateRun(_entry) {
	// no-op: gate runs now written to JSONL trace files
}

export function upsertTurnGitTransaction(_entry) {
	// no-op: turn git transactions now written to JSONL audit events
}

export function getGateRunStats(gateId, windowHours = 24) {
	try {
		const currentPath = getDbPath();
		const basePath =
			currentPath && currentPath !== ":memory:"
				? dirname(dirname(currentPath))
				: process.cwd();
		const events = readTraceEvents(basePath, "gate_run", windowHours).filter(
			(e) => e.gateId === gateId,
		);
		if (events.length > 0) {
			const stats = {
				total: events.length,
				pass: 0,
				fail: 0,
				retry: 0,
				manualAttention: 0,
				lastEvaluatedAt: null,
			};
			for (const e of events) {
				if (e.outcome === "pass") stats.pass++;
				else if (e.outcome === "fail") stats.fail++;
				else if (e.outcome === "retry") stats.retry++;
				else if (e.outcome === "manual-attention") stats.manualAttention++;
				if (
					!stats.lastEvaluatedAt ||
					(e.evaluatedAt ?? e.ts) > stats.lastEvaluatedAt
				)
					stats.lastEvaluatedAt = e.evaluatedAt ?? e.ts;
			}
			return stats;
		}
		// Fall back to quality_gates DB when no trace events found (e.g. after trace rotation)
		const db = _getAdapter();
		if (db) {
			const cutoff = new Date(
				Date.now() - windowHours * 3600 * 1000,
			).toISOString();
			const rows = db
				.prepare(
					`SELECT verdict, evaluated_at FROM quality_gates
					 WHERE gate_id = ? AND evaluated_at >= ? AND verdict != '' AND verdict != 'omitted'
					 ORDER BY evaluated_at DESC`,
				)
				.all(gateId, cutoff);
			if (rows.length > 0) {
				const stats = {
					total: rows.length,
					pass: 0,
					fail: 0,
					retry: 0,
					manualAttention: 0,
					lastEvaluatedAt: rows[0].evaluated_at,
				};
				for (const r of rows) {
					if (r.verdict === "pass") stats.pass++;
					else if (r.verdict === "fail" || r.verdict === "flag") stats.fail++;
					else if (r.verdict === "manual-attention") stats.manualAttention++;
				}
				return stats;
			}
		}
		return {
			total: 0,
			pass: 0,
			fail: 0,
			retry: 0,
			manualAttention: 0,
			lastEvaluatedAt: null,
		};
	} catch {
		return {
			total: 0,
			pass: 0,
			fail: 0,
			retry: 0,
			manualAttention: 0,
			lastEvaluatedAt: null,
		};
	}
}

export function getGateCircuitBreaker(gateId) {
	const currentDb = _getAdapter();
	if (!currentDb) {
		return {
			gateId,
			state: "closed",
			failureStreak: 0,
			lastFailureAt: null,
			openedAt: null,
			halfOpenAttempts: 0,
			updatedAt: null,
		};
	}
	try {
		const row = currentDb
			.prepare(
				`SELECT gate_id, state, failure_streak, last_failure_at, opened_at, half_open_attempts, updated_at
				 FROM gate_circuit_breakers
				 WHERE gate_id = :gate_id`,
			)
			.get({ ":gate_id": gateId });
		if (!row) {
			return {
				gateId,
				state: "closed",
				failureStreak: 0,
				lastFailureAt: null,
				openedAt: null,
				halfOpenAttempts: 0,
				updatedAt: null,
			};
		}
		return {
			gateId: row.gate_id,
			state: row.state,
			failureStreak: row.failure_streak ?? 0,
			lastFailureAt: row.last_failure_at ?? null,
			openedAt: row.opened_at ?? null,
			halfOpenAttempts: row.half_open_attempts ?? 0,
			updatedAt: row.updated_at ?? null,
		};
	} catch {
		return {
			gateId,
			state: "closed",
			failureStreak: 0,
			lastFailureAt: null,
			openedAt: null,
			halfOpenAttempts: 0,
			updatedAt: null,
		};
	}
}

export function updateGateCircuitBreaker(gateId, updates) {
	const currentDb = _getAdapter();
	if (!currentDb) return;
	currentDb
		.prepare(
			`INSERT INTO gate_circuit_breakers (
				   gate_id, state, failure_streak, last_failure_at, opened_at, half_open_attempts, updated_at
				 ) VALUES (
				   :gate_id, :state, :failure_streak, :last_failure_at, :opened_at, :half_open_attempts, :updated_at
				 )
				 ON CONFLICT(gate_id) DO UPDATE SET
				   state = excluded.state,
				   failure_streak = excluded.failure_streak,
				   last_failure_at = COALESCE(excluded.last_failure_at, gate_circuit_breakers.last_failure_at),
				   opened_at = COALESCE(excluded.opened_at, gate_circuit_breakers.opened_at),
				   half_open_attempts = excluded.half_open_attempts,
				   updated_at = excluded.updated_at`,
		)
		.run({
			":gate_id": gateId,
			":state": updates.state ?? "closed",
			":failure_streak": updates.failureStreak ?? 0,
			":last_failure_at": updates.lastFailureAt ?? null,
			":opened_at": updates.openedAt ?? null,
			":half_open_attempts": updates.halfOpenAttempts ?? 0,
			":updated_at": new Date().toISOString(),
		});
	return { total: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 };
}

export function getGateLatencyStats(gateId, windowHours = 24) {
	try {
		const currentPath = getDbPath();
		const basePath =
			currentPath && currentPath !== ":memory:"
				? dirname(dirname(currentPath))
				: process.cwd();
		const durations = readTraceEvents(basePath, "gate_run", windowHours)
			.filter((e) => e.gateId === gateId && typeof e.durationMs === "number")
			.map((e) => e.durationMs)
			.sort((a, b) => a - b);
		if (durations.length === 0)
			return {
				p50: null,
				p95: null,
				count: 0,
				total: 0,
				avgMs: 0,
				p50Ms: 0,
				p95Ms: 0,
				maxMs: 0,
			};
		const p50Ms = durations[Math.floor(durations.length * 0.5)] ?? 0;
		const p95Ms = durations[Math.floor(durations.length * 0.95)] ?? 0;
		const maxMs = durations[durations.length - 1] ?? 0;
		const avgMs = Math.round(
			durations.reduce((s, v) => s + v, 0) / durations.length,
		);
		return {
			p50: p50Ms,
			p95: p95Ms,
			count: durations.length,
			total: durations.length,
			avgMs,
			p50Ms,
			p95Ms,
			maxMs,
		};
	} catch {
		return {
			p50: null,
			p95: null,
			count: 0,
			total: 0,
			avgMs: 0,
			p50Ms: 0,
			p95Ms: 0,
			maxMs: 0,
		};
	}
}

export function getDistinctGateIds() {
	// UNION trace-event IDs with quality_gates DB IDs (codex review 2026-05-14
	// follow-up on slice 3a). The previous implementation returned trace IDs
	// only when any existed, silently hiding DB-only slice-scoped gates the
	// moment any flow-scoped trace landed. Status uok must show both.
	const ids = new Set();
	try {
		const currentPath = getDbPath();
		const basePath =
			currentPath && currentPath !== ":memory:"
				? dirname(dirname(currentPath))
				: process.cwd();
		const events = readTraceEvents(basePath, "gate_run", 24 * 30); // 30 days
		for (const ev of events) {
			if (typeof ev.gateId === "string" && ev.gateId.length > 0) {
				ids.add(ev.gateId);
			}
		}
	} catch {
		// Trace read failure is non-fatal — DB fallback below still runs.
	}
	try {
		const db = _getAdapter();
		if (db) {
			const rows = db
				.prepare(
					"SELECT DISTINCT gate_id FROM quality_gates WHERE gate_id != '' ORDER BY gate_id",
				)
				.all();
			for (const r of rows) {
				if (typeof r.gate_id === "string" && r.gate_id.length > 0) {
					ids.add(r.gate_id);
				}
			}
		}
	} catch {
		// DB read failure is non-fatal — trace results above stand.
	}
	return Array.from(ids).sort();
}

export function upsertQualityGate(g) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(`INSERT OR REPLACE INTO quality_gates
     (milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
     VALUES (:mid, :sid, :gid, :scope, :tid, :status, :verdict, :rationale, :findings, :evaluated_at)`)
		.run({
			":mid": g.milestoneId,
			":sid": g.sliceId,
			":gid": g.gateId,
			":scope": g.scope,
			":tid": g.taskId,
			":status": g.status,
			":verdict": g.verdict,
			":rationale": g.rationale,
			":findings": g.findings,
			":evaluated_at": g.evaluatedAt,
		});
}
