// SF — Persistent per-project blocklist of provider/model pairs that the
// provider has rejected at request time for account entitlement or temporary
// capacity reasons.
//
// Lives at `.sf/runtime/blocked-models.json` so the block survives /autonomous
// restarts.  Autonomous mode model selection skips blocked entries; agent-end
// recovery adds entries when a runtime rejection is classified as
// `unsupported-model`.  See issue #4513.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { withFileLockSync } from "./file-lock.js";
import { sfRoot } from "./paths.js";

function blockedModelsPath(basePath) {
	return join(sfRoot(basePath), "runtime", "blocked-models.json");
}
function modelKey(provider, id) {
	return `${provider.toLowerCase()}/${id.toLowerCase()}`;
}
function activeBlockedEntries(entries, now = Date.now()) {
	return entries.filter(
		(e) =>
			typeof e.expiresAt !== "number" ||
			!Number.isFinite(e.expiresAt) ||
			e.expiresAt > now,
	);
}
function readFileSafe(path) {
	if (!existsSync(path)) return { version: 1, blocked: [] };
	try {
		const raw = readFileSync(path, "utf-8");
		const parsed = JSON.parse(raw);
		if (!parsed || !Array.isArray(parsed.blocked)) {
			return { version: 1, blocked: [] };
		}
		const blocked = parsed.blocked.filter(
			(e) => !!e && typeof e.provider === "string" && typeof e.id === "string",
		);
		return { version: 1, blocked: activeBlockedEntries(blocked) };
	} catch {
		// Corrupted JSON: treat as empty so a bad file never blocks dispatch.
		return { version: 1, blocked: [] };
	}
}
export function loadBlockedModels(basePath) {
	return readFileSafe(blockedModelsPath(basePath)).blocked;
}
export function isModelBlocked(basePath, provider, id) {
	if (!provider || !id) return false;
	const target = modelKey(provider, id);
	return loadBlockedModels(basePath).some(
		(e) => modelKey(e.provider, e.id) === target,
	);
}
/**
 * Add a provider/model pair to the persistent blocklist (e.g., after account entitlement rejection).
 */
export function blockModel(basePath, provider, id, reason, options = {}) {
	const path = blockedModelsPath(basePath);
	mkdirSync(dirname(path), { recursive: true });
	// Ensure the file exists before we try to lock it — proper-lockfile requires
	// the target path to exist (file-lock.ts falls through to an unlocked call
	// otherwise).
	if (!existsSync(path)) {
		writeFileSync(
			path,
			JSON.stringify({ version: 1, blocked: [] }, null, 2) + "\n",
			"utf-8",
		);
	}
	withFileLockSync(path, () => {
		const current = readFileSafe(path);
		const target = modelKey(provider, id);
		const existing = current.blocked.find(
			(e) => modelKey(e.provider, e.id) === target,
		);
		if (existing) {
			if (
				typeof options.expiresAt === "number" &&
				(!existing.expiresAt || options.expiresAt > existing.expiresAt)
			) {
				existing.expiresAt = options.expiresAt;
				existing.reason = reason;
				writeFileSync(path, JSON.stringify(current, null, 2) + "\n", "utf-8");
			}
			return;
		}
		const expiresAt =
			typeof options.expiresAt === "number" &&
			Number.isFinite(options.expiresAt)
				? options.expiresAt
				: undefined;
		const next = {
			version: 1,
			blocked: [
				...current.blocked,
				{ provider, id, reason, blockedAt: Date.now(), expiresAt },
			],
		};
		writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
	});
}
