/**
 * Upstream-feedback → forge-self-feedback bridge.
 *
 * Rolls up recurring upstream anomalies (observed while sf runs on external
 * repos) into the forge-local self-self-feedback so they can be triaged and
 * addressed as forge-side fixes.
 *
 * Called from register-hooks.ts session_start drain (wired externally).
 * Never throws — any I/O failure returns 0.
 */
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { readAllSelfFeedback, recordSelfFeedback } from "./self-feedback.js";
import { sfHome } from "./sf-home.js";

// ─── Constants ────────────────────────────────────────────────────────────────
const SEVERITY_ORDER = ["low", "medium", "high", "critical"];
const ROLLUP_CAP = "medium";
const THRESHOLD_COUNT = 3;
const THRESHOLD_REPOS = 2;
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
// ─── Helpers ──────────────────────────────────────────────────────────────────
function getUpstreamLogPath() {
	return join(sfHome(), "agent", "upstream-feedback.jsonl");
}
function isForgeRepo(basePath) {
	try {
		const pkgPath = join(basePath, "package.json");
		if (!existsSync(pkgPath)) return false;
		const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
		return pkg?.name === "singularity-forge";
	} catch {
		return false;
	}
}
function readUpstreamEntries() {
	const path = getUpstreamLogPath();
	try {
		if (!existsSync(path)) return [];
		const out = [];
		for (const line of readFileSync(path, "utf-8").split("\n")) {
			if (!line.trim()) continue;
			try {
				out.push(JSON.parse(line));
			} catch {
				/* skip malformed lines */
			}
		}
		return out;
	} catch {
		return [];
	}
}
function capSeverity(sev) {
	const idx = SEVERITY_ORDER.indexOf(sev);
	const capIdx = SEVERITY_ORDER.indexOf(ROLLUP_CAP);
	return SEVERITY_ORDER[Math.min(idx, capIdx)];
}
function maxSeverity(entries) {
	let max = 0;
	for (const e of entries) {
		const idx = SEVERITY_ORDER.indexOf(e.severity);
		if (idx > max) max = idx;
	}
	return SEVERITY_ORDER[max];
}
// ─── Public API ───────────────────────────────────────────────────────────────
/**
 * Roll up upstream feedback entries into the forge-local self-feedback.
 * Only runs when basePath is the singularity-forge repo itself.
 *
 * @returns count of new rollup entries filed (0 on bail/failure)
 */
export function bridgeUpstreamFeedback(basePath = process.cwd()) {
	try {
		if (!isForgeRepo(basePath)) return 0;
		const cutoff = Date.now() - THIRTY_DAYS_MS;
		const upstream = readUpstreamEntries().filter(
			(e) =>
				!e.resolvedAt &&
				e.repoIdentity === "external" &&
				new Date(e.ts).getTime() >= cutoff,
		);
		if (upstream.length === 0) return 0;
		// Group by kind, then compute distinct basePaths per group
		const byKind = new Map();
		for (const e of upstream) {
			const list = byKind.get(e.kind) ?? [];
			list.push(e);
			byKind.set(e.kind, list);
		}
		// Read existing forge-local entries once for idempotency checks. We
		// exclude both currently-OPEN rollup kinds AND rollup kinds that an
		// operator (or the triage worker) closed within the last LOOKBACK_DAYS
		// window. Without the closed-recently check, the bridge re-files the
		// same cluster on the next session_start every time someone closes it
		// — observed in dogfood 2026-05-13 (sf-mp4rp6xn-hpag5h). The closure
		// is treated as time-limited: after the window expires, a re-cluster
		// CAN re-file (the original closure was made against then-current
		// state; later state may legitimately surface the same kind again).
		const existing = readAllSelfFeedback(basePath);
		const recentCutoff = Date.now() - THIRTY_DAYS_MS;
		const suppressedRollupKinds = new Set();
		for (const e of existing) {
			if (!e.kind?.startsWith("upstream-rollup:")) continue;
			if (!e.resolvedAt) {
				suppressedRollupKinds.add(e.kind);
				continue;
			}
			const resolvedMs = new Date(e.resolvedAt).getTime();
			if (Number.isFinite(resolvedMs) && resolvedMs >= recentCutoff) {
				suppressedRollupKinds.add(e.kind);
			}
		}
		let filed = 0;
		for (const [kind, entries] of byKind) {
			if (entries.length < THRESHOLD_COUNT) continue;
			const distinctRepos = new Set(entries.map((e) => e.basePath)).size;
			if (distinctRepos < THRESHOLD_REPOS) continue;
			const rollupKind = `upstream-rollup:${kind}`;
			if (suppressedRollupKinds.has(rollupKind)) continue;
			// Derive severity, capped at medium
			const severity = capSeverity(maxSeverity(entries));
			// Build evidence block: up to 5 samples + full id list
			const samples = entries.slice(0, 5);
			const sampleLines = samples
				.map((e) => `  [${e.id}] ${e.basePath} — ${e.summary}`)
				.join("\n");
			const allIds = entries.map((e) => e.id).join(", ");
			const evidence =
				`Samples (${samples.length} of ${entries.length}):\n${sampleLines}\n\n` +
				`All upstream ids: ${allIds}`;
			const result = recordSelfFeedback(
				{
					kind: rollupKind,
					severity,
					summary: `${entries.length} external-repo entries of kind '${kind}' across ${distinctRepos} repos`,
					evidence,
					suggestedFix:
						"Triage the cluster — common kinds suggest a forge-side fix (threshold tweak, gate fix, prompt clarification). Mark resolved with kind: 'agent-fix' citing the commit, or 'human-clear' with reason.",
					source: "detector",
					occurredIn: undefined,
				},
				basePath,
			);
			if (result) {
				filed++;
				suppressedRollupKinds.add(rollupKind); // prevent double-filing in same run
			}
		}
		return filed;
	} catch {
		return 0;
	}
}
