/**
 * Remote Questions — orchestration manager
 */
import { randomUUID } from "node:crypto";
import {
	formatRoundResultForTool,
	roundResultFromRemoteAnswer,
} from "@singularity-forge/agent-core";
import { sanitizeError } from "../shared/sanitize.js";
import {
	resolveRemoteConfig,
	resolveRemotePreferenceConfig,
} from "./config.js";
import { DiscordAdapter } from "./discord-adapter.js";
import { SlackAdapter } from "./slack-adapter.js";
import {
	createPromptRecord,
	markPromptAnswered,
	markPromptDispatched,
	markPromptStatus,
	updatePromptRecord,
	writePromptRecord,
} from "./store.js";
import { TelegramAdapter } from "./telegram-adapter.js";
export function tryAutoResolveQuestions(
	questions,
	strategy = "recommended-option",
) {
	if (strategy !== "recommended-option") return null;
	const answers = {};
	for (const question of questions) {
		if (question.allowMultiple) return null;
		const firstOption = question.options[0];
		if (!firstOption?.label) return null;
		answers[question.id] = { answers: [firstOption.label] };
	}
	return { answers };
}
export function resolveHeadlessLocalAutoResolvePolicy() {
	const config = resolveRemotePreferenceConfig();
	if (!config || config.channel !== "telegram") return null;
	return {
		channel: "telegram",
		timeoutMs: config.timeoutMs,
		autoResolveOnTimeout: config.autoResolveOnTimeout,
		autoResolveStrategy: config.autoResolveStrategy,
	};
}
export async function tryHeadlessLocalAutoResolveQuestions(questions, options) {
	const policy = options.policy ?? resolveHeadlessLocalAutoResolvePolicy();
	if (options.hasUI || !options.telegramUnavailable) return null;
	if (!policy?.autoResolveOnTimeout) return null;
	if (options.signal?.aborted) return null;
	await (options.sleepFn ?? sleep)(policy.timeoutMs, options.signal);
	if (options.signal?.aborted) return null;
	const autoResolved = tryAutoResolveQuestions(
		questions,
		policy.autoResolveStrategy,
	);
	if (!autoResolved) return null;
	const resolved = resultFromRemoteAnswer(autoResolved, questions);
	return {
		content: resolved.content,
		details: {
			remote: true,
			channel: policy.channel,
			timed_out: true,
			status: "auto-resolved-local",
			autoResolved: true,
			autoResolveStrategy: policy.autoResolveStrategy,
			localFallback: true,
			unavailableReason: options.unavailableReason,
			questions,
			response: resolved.response,
		},
	};
}
/**
 * Check whether a remote channel is configured without triggering any
 * side effects (no HTTP requests, no prompt records). Used by the race
 * logic to decide routing before committing to a remote dispatch.
 */
export function isRemoteConfigured() {
	return resolveRemoteConfig() !== null;
}
export async function tryRemoteQuestions(questions, signal) {
	const config = resolveRemoteConfig();
	if (!config) return null;
	const prompt = createPrompt(questions, config);
	writePromptRecord(createPromptRecord(prompt));
	const adapter = createAdapter(config);
	try {
		await adapter.validate();
	} catch (err) {
		markPromptStatus(prompt.id, "failed", sanitizeError(String(err.message)));
		return errorResult(
			`Remote auth failed (${config.channel}): ${err.message}`,
			config.channel,
		);
	}
	let dispatch;
	try {
		dispatch = await adapter.sendPrompt(prompt);
		markPromptDispatched(prompt.id, dispatch.ref);
	} catch (err) {
		markPromptStatus(prompt.id, "failed", sanitizeError(String(err.message)));
		return errorResult(
			`Failed to send questions via ${config.channel}: ${err.message}`,
			config.channel,
		);
	}
	const pollResult = await pollUntilDone(adapter, prompt, dispatch.ref, signal);
	if (!pollResult.answer) {
		if (
			!signal?.aborted &&
			pollResult.unavailable &&
			config.channel === "telegram" &&
			config.autoResolveOnTimeout
		) {
			await sleep(Math.max(0, prompt.timeoutAt - Date.now()), signal);
		}
		const autoResolved =
			!signal?.aborted && config.autoResolveOnTimeout
				? tryAutoResolveQuestions(questions, config.autoResolveStrategy)
				: null;
		if (autoResolved) {
			markPromptAnswered(prompt.id, autoResolved);
			const resolved = resultFromRemoteAnswer(autoResolved, questions);
			return {
				content: resolved.content,
				details: {
					remote: true,
					channel: config.channel,
					timed_out: true,
					promptId: prompt.id,
					threadUrl: dispatch.ref.threadUrl ?? null,
					status: "auto-resolved",
					autoResolved: true,
					autoResolveStrategy: config.autoResolveStrategy,
					questions,
					response: resolved.response,
				},
			};
		}
		markPromptStatus(prompt.id, signal?.aborted ? "cancelled" : "timed_out");
		return {
			content: [
				{
					type: "text",
					text: JSON.stringify({
						timed_out: true,
						channel: config.channel,
						prompt_id: prompt.id,
						timeout_minutes: config.timeoutMs / 60000,
						thread_url: dispatch.ref.threadUrl ?? null,
						message: `User did not respond within ${config.timeoutMs / 60000} minutes.`,
					}),
				},
			],
			details: {
				remote: true,
				channel: config.channel,
				timed_out: true,
				promptId: prompt.id,
				threadUrl: dispatch.ref.threadUrl ?? null,
				status: signal?.aborted ? "cancelled" : "timed_out",
			},
		};
	}
	markPromptAnswered(prompt.id, pollResult.answer);
	// Best-effort acknowledgement gives remote users a visible receipt signal.
	try {
		await adapter.acknowledgeAnswer?.(dispatch.ref);
	} catch {
		/* best-effort */
	}
	const resolved = resultFromRemoteAnswer(pollResult.answer, questions);
	return {
		content: resolved.content,
		details: {
			remote: true,
			channel: config.channel,
			timed_out: false,
			promptId: prompt.id,
			threadUrl: dispatch.ref.threadUrl ?? null,
			questions,
			response: resolved.response,
			status: "answered",
		},
	};
}
function createPrompt(questions, config) {
	const createdAt = Date.now();
	return {
		id: randomUUID(),
		channel: config.channel,
		createdAt,
		timeoutAt: createdAt + config.timeoutMs,
		pollIntervalMs: config.pollIntervalMs,
		context: { source: "ask_user_questions" },
		questions: questions.map((q) => ({
			id: q.id,
			header: q.header,
			question: q.question,
			options: q.options,
			allowMultiple: q.allowMultiple ?? false,
		})),
	};
}
function resultFromRemoteAnswer(answer, questions) {
	const response = roundResultFromRemoteAnswer(answer, questions);
	return {
		content: [{ type: "text", text: formatRoundResultForTool(response) }],
		response,
	};
}
function createAdapter(config) {
	if (config.channel === "slack")
		return new SlackAdapter(config.token, config.channelId);
	if (config.channel === "telegram")
		return new TelegramAdapter(
			config.token,
			config.channelId,
			config.allowedUserIds,
		);
	return new DiscordAdapter(config.token, config.channelId);
}
async function pollUntilDone(adapter, prompt, ref, signal) {
	let retryCount = 0;
	while (Date.now() < prompt.timeoutAt && !signal?.aborted) {
		try {
			const answer = await adapter.pollAnswer(prompt, ref);
			updatePromptRecord(prompt.id, { lastPollAt: Date.now() });
			retryCount = 0;
			if (answer) return { answer };
		} catch (err) {
			retryCount++;
			if (retryCount > 1) {
				const message = sanitizeError(String(err.message));
				markPromptStatus(prompt.id, "failed", message);
				return { answer: null, unavailable: true, error: message };
			}
		}
		await sleep(prompt.pollIntervalMs, signal);
	}
	return { answer: null };
}
function sleep(ms, signal) {
	return new Promise((resolve) => {
		if (signal?.aborted) return resolve();
		const timer = setTimeout(() => {
			if (signal) signal.removeEventListener("abort", onAbort);
			resolve();
		}, ms);
		const onAbort = () => {
			clearTimeout(timer);
			resolve();
		};
		signal?.addEventListener("abort", onAbort, { once: true });
	});
}
function errorResult(message, channel) {
	return {
		content: [{ type: "text", text: sanitizeError(message) }],
		details: { remote: true, channel, error: true, status: "failed" },
	};
}
