import { getCredentialVault } from "./credential-vault.mjs";
import { guardedFetch } from "./endpoint-allowlist.mjs";
//#region extensions/crypto/src/services/fly-control-service.ts
/**
* Fly Control Service — Manages the Fly.io deployment from within the bot.
*
* Uses the Fly Machines REST API (https://api.machines.dev) to:
* - List/set/delete app-level secrets
* - Get machine status
* - Restart the machine
*
* Auth: FLY_API_TOKEN env var (set as a Fly secret itself).
* App name: FLY_APP_NAME env var (or auto-detected from FLY_APP_NAME on Fly).
*
* SECURITY: All callers must be authenticated (requireAuth on commands).
* Secret values are write-only — Fly's API never returns plaintext values.
*/
const FLY_API_BASE = "https://api.machines.dev/v1";
function getFlyToken() {
	return getCredentialVault().getSecret("deploy.fly.apiToken", "fly-control");
}
function getFlyAppName() {
	return process.env.FLY_APP_NAME ?? null;
}
function isFlyControlAvailable() {
	return !!(getFlyToken() && getFlyAppName());
}
var FlyControlError = class extends Error {
	constructor(message, statusCode) {
		super(message);
		this.statusCode = statusCode;
		this.name = "FlyControlError";
	}
};
var FlyNotConfiguredError = class extends FlyControlError {
	constructor() {
		super("Fly control not configured. Set FLY_API_TOKEN as a Fly secret:\n  fly secrets set FLY_API_TOKEN=\"$(fly tokens create deploy -a <your-app>)\" -a <your-app>");
	}
};
function requireConfig() {
	const token = getFlyToken();
	const appName = getFlyAppName();
	if (!token) throw new FlyNotConfiguredError();
	if (!appName) throw new FlyControlError("FLY_APP_NAME not set. Are you running on Fly.io?");
	return {
		token,
		appName
	};
}
async function flyRequest(method, path, body) {
	const { token, appName } = requireConfig();
	const res = await guardedFetch(`${FLY_API_BASE}/apps/${appName}${path}`, {
		method,
		headers: {
			"Authorization": `Bearer ${token}`,
			"Content-Type": "application/json"
		},
		...body !== void 0 ? { body: JSON.stringify(body) } : {},
		signal: AbortSignal.timeout(3e4)
	});
	if (res.ok) {
		const text = await res.text();
		if (!text) return null;
		try {
			return JSON.parse(text);
		} catch {
			return text;
		}
	}
	const errBody = await res.text().catch(() => "");
	if (res.status === 401) throw new FlyControlError("Fly API auth failed. Your FLY_API_TOKEN may be expired.\nGenerate a new one: fly tokens create deploy -a <your-app>");
	if (res.status === 404) throw new FlyControlError(`Not found: ${path}`);
	throw new FlyControlError(`Fly API error ${res.status}: ${errBody || "Unknown error"}`, res.status);
}
/** List all secrets (names + digests only, never plaintext values). */
async function listSecrets() {
	const data = await flyRequest("GET", "/secrets");
	return (Array.isArray(data) ? data : data?.secrets ?? []).map((s) => ({
		name: s.name ?? s.Name,
		digest: s.digest ?? s.Digest ?? "",
		createdAt: s.created_at ?? s.CreatedAt ?? "",
		version: s.version ?? s.Version
	}));
}
/**
* Set one or more secrets. Values are encrypted by Fly and never returned.
* Returns the new secrets version number — pass this to redeployMachine()
* so the machine picks up the staged secrets.
*/
async function setSecrets(secrets) {
	const result = await flyRequest("POST", "/secrets", { values: secrets });
	return result?.version ?? result?.Version ?? void 0;
}
/** Delete a single secret. */
async function deleteSecret(name) {
	await flyRequest("DELETE", `/secrets/${name}`);
}
/** List machines for this app. */
async function listMachines() {
	const data = await flyRequest("GET", "/machines");
	return (Array.isArray(data) ? data : []).map((m) => ({
		id: m.id,
		name: m.name ?? "",
		state: m.state ?? "unknown",
		region: m.region ?? "",
		instanceId: m.instance_id ?? "",
		privateIp: m.private_ip ?? "",
		createdAt: m.created_at ?? "",
		updatedAt: m.updated_at ?? "",
		imageRef: m.config?.image ?? m.image_ref?.repository ?? "",
		cpuKind: m.config?.guest?.cpu_kind ?? "",
		cpus: m.config?.guest?.cpus ?? 0,
		memoryMb: m.config?.guest?.memory_mb ?? 0
	}));
}
/** Restart a specific machine (simple restart, does NOT pick up new secrets). */
async function restartMachine(machineId) {
	await flyRequest("POST", `/machines/${machineId}/restart?timeout=30s`);
}
/**
* Update a machine in-place (GET config → POST it back) with min_secrets_version.
*
* The Fly Machines API stages secrets on set but does NOT deploy them until
* a machine update includes `min_secrets_version` matching the staged version.
* This is what `fly secrets deploy` does under the hood.
*
* If no secretsVersion is passed, we first query the secrets list to get the
* latest version automatically.
*/
async function redeployMachine(machineId, secretsVersion) {
	const config = (await flyRequest("GET", `/machines/${machineId}`))?.config;
	if (!config) throw new FlyControlError(`Machine ${machineId} has no config`);
	let minSecretsVersion = secretsVersion;
	if (minSecretsVersion === void 0) {
		const secrets = await listSecrets();
		for (const s of secrets) if (s.version !== void 0 && (minSecretsVersion === void 0 || s.version > minSecretsVersion)) minSecretsVersion = s.version;
	}
	const body = { config };
	if (minSecretsVersion !== void 0) body.min_secrets_version = minSecretsVersion;
	await flyRequest("POST", `/machines/${machineId}`, body);
}
/**
* Redeploy all running machines (picks up staged secrets).
* Uses update with min_secrets_version (not restart) to ensure new secrets are deployed.
*
* @param secretsVersion - If provided, passed to redeployMachine. Otherwise auto-detected.
*/
async function restartAllMachines(secretsVersion) {
	const running = (await listMachines()).filter((m) => m.state === "started");
	const redeployed = [];
	let version = secretsVersion;
	if (version === void 0) {
		const secrets = await listSecrets();
		for (const s of secrets) if (s.version !== void 0 && (version === void 0 || s.version > version)) version = s.version;
	}
	for (const m of running) try {
		await redeployMachine(m.id, version);
		redeployed.push(m.id);
	} catch (err) {
		try {
			await restartMachine(m.id);
			redeployed.push(m.id);
		} catch (err2) {
			console.error(`Failed to redeploy/restart machine ${m.id}: ${err2}`);
		}
	}
	return redeployed;
}
const VALID_PROVIDERS = [
	"anthropic",
	"bankr",
	"openrouter",
	"openai"
];
function isValidProvider(p) {
	return VALID_PROVIDERS.includes(p);
}
/**
* Set the LLM provider secret (without restarting).
* Returns the new secrets version — pass to scheduleRestart() so the
* machine update includes min_secrets_version and actually deploys it.
*/
async function setProvider(provider) {
	if (!isValidProvider(provider)) throw new FlyControlError(`Invalid provider: ${provider}. Valid: ${VALID_PROVIDERS.join(", ")}`);
	return await setSecrets({ OPENCLAWNCH_LLM_PROVIDER: provider });
}
/**
* Schedule a restart after a delay (default 2s).
* This gives the command response time to be delivered before the process dies.
* Fire-and-forget — errors are logged but don't propagate.
*
* @param delayMs - Delay before restart (default 2000ms)
* @param secretsVersion - If provided, ensures the machine update includes
*   min_secrets_version so staged secrets are deployed (not just staged).
*/
function scheduleRestart(delayMs = 2e3, secretsVersion) {
	setTimeout(async () => {
		try {
			await restartAllMachines(secretsVersion);
		} catch (err) {
			console.error("[fly-control] Scheduled restart failed:", err);
		}
	}, delayMs);
}
function getCurrentProvider() {
	const p = process.env.OPENCLAWNCH_LLM_PROVIDER;
	if (p && isValidProvider(p)) return p;
	return "anthropic";
}
//#endregion
export { FlyControlError, FlyNotConfiguredError, deleteSecret, getCurrentProvider, isFlyControlAvailable, isValidProvider, listMachines, listSecrets, redeployMachine, restartAllMachines, restartMachine, scheduleRestart, setProvider, setSecrets };

//# sourceMappingURL=fly-control-service.mjs.map