/**
 * Twilio IVR webhook for customer calls into the SF voice bridge.
 *
 * Purpose: give the public Twilio number a deterministic menu that can route
 * callers to the on-call line or to an optional AI assistant without adding a
 * separate voice service.
 *
 * Consumer: Twilio Programmable Voice webhook hits this route on inbound calls.
 */

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

type VoiceRoutingConfig = {
	onCallNumbers: string[];
	aiNumbers: string[];
};

type VoicePromptConfig = {
	useElevenLabs: boolean;
	voiceId: string | null;
	modelId: string;
	apiKey: string | null;
};

function noStoreHeaders(): HeadersInit {
	return {
		"Cache-Control": "no-store",
		"Content-Type": "text/xml; charset=utf-8",
	};
}

function escapeXml(value: string): string {
	return value
		.replace(/&/g, "&amp;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&apos;");
}

function splitPhoneList(value: string | undefined): string[] {
	if (!value) return [];
	return value
		.split(/[\s,]+/)
		.map((entry) => entry.trim())
		.filter(Boolean);
}

function getRoutingConfig(): VoiceRoutingConfig {
	const env = process.env;
	const onCallNumbers = splitPhoneList(
		env.TWILIO_IVR_ONCALL_NUMBERS ??
			env.TWILIO_IVR_ONCALL_NUMBER ??
			env.TWILIO_ONCALL_NUMBER ??
			env.ONCALL_NUMBER,
	);
	const aiNumbers = splitPhoneList(
		env.TWILIO_IVR_AI_NUMBERS ?? env.TWILIO_IVR_AI_NUMBER,
	);
	return { aiNumbers, onCallNumbers };
}

function getPromptConfig(): VoicePromptConfig {
	const env = process.env;
	const apiKey = env.ELEVENLABS_API_KEY?.trim() ?? null;
	const voiceId = env.ELEVENLABS_VOICE_ID?.trim() ?? null;
	return {
		apiKey,
		modelId: env.ELEVENLABS_MODEL_ID?.trim() || "eleven_flash_v2_5",
		useElevenLabs: Boolean(apiKey && voiceId),
		voiceId,
	};
}

function buildTwiml(body: string): string {
	return `<?xml version="1.0" encoding="UTF-8"?>\n<Response>\n${body}\n</Response>\n`;
}

function buildMenuPrompt(config: VoiceRoutingConfig): string {
	const lines = [
		`Welcome to Singularity Forge.`,
		`Press 1 to reach the on-call line.`,
	];
	if (config.aiNumbers.length > 0) {
		lines.push(`Press 2 for the AI assistant.`);
	}
	lines.push(`Press 9 to repeat this menu.`);
	return lines.join(" ");
}

function buildPromptVerb(promptText: string, requestUrl: string): string {
	const promptConfig = getPromptConfig();
	if (!promptConfig.useElevenLabs) {
		return `<Say voice="alice" language="en-US">${escapeXml(promptText)}</Say>`;
	}
	const promptUrl = new URL("/api/voice/prompt", requestUrl);
	promptUrl.searchParams.set("text", promptText);
	return `<Play>${escapeXml(promptUrl.toString())}</Play>`;
}

function buildDialTargets(numbers: string[]): string {
	if (numbers.length === 0) {
		return `<Say voice="alice" language="en-US">The requested destination is not configured.</Say>\n<Hangup/>`;
	}
	return [
		`<Dial answerOnBridge="true" timeout="20">`,
		...numbers.map((number) => `  <Number>${escapeXml(number)}</Number>`),
		`</Dial>`,
	].join("\n");
}

function buildMenuResponse(
	requestUrl: string,
	config: VoiceRoutingConfig,
): string {
	return buildTwiml(
		[
			`<Gather input="dtmf" numDigits="1" timeout="6" action="/api/voice" method="POST">`,
			`  ${buildPromptVerb(buildMenuPrompt(config), requestUrl)}`,
			`</Gather>`,
			`<Say voice="alice" language="en-US">Sorry, we did not get a selection.</Say>`,
			`<Redirect method="POST">/api/voice</Redirect>`,
		].join("\n"),
	);
}

function buildDigitsResponse(
	digits: string | null,
	requestUrl: string,
	config: VoiceRoutingConfig,
): string {
	if (digits === "1") {
		return buildTwiml(buildDialTargets(config.onCallNumbers));
	}
	if (digits === "2" && config.aiNumbers.length > 0) {
		return buildTwiml(buildDialTargets(config.aiNumbers));
	}
	return buildMenuResponse(requestUrl, config);
}

async function readDigits(request: Request): Promise<string | null> {
	if (request.method === "GET") return null;
	const body = await request.text();
	if (!body) return null;
	const params = new URLSearchParams(body);
	return params.get("Digits") ?? params.get("digits");
}

/**
 * Return the initial IVR menu for inbound Twilio calls.
 *
 * Purpose: present a stable entrypoint for customers so the Twilio number can
 * route them either to on-call or to an optional AI assistant.
 *
 * Consumer: Twilio inbound call webhook and local integration tests.
 */
export async function GET(request: Request): Promise<Response> {
	return new Response(buildMenuResponse(request.url, getRoutingConfig()), {
		headers: noStoreHeaders(),
	});
}

/**
 * Route the Twilio call according to the selected IVR digit.
 *
 * Purpose: let the same public number handle both menu playback and the actual
 * customer transfer without adding a second webhook surface.
 *
 * Consumer: Twilio posts DTMF selections back to this route after the caller
 * chooses an IVR option.
 */
export async function POST(request: Request): Promise<Response> {
	const digits = await readDigits(request);
	const responseXml =
		digits === null
			? buildMenuResponse(request.url, getRoutingConfig())
			: buildDigitsResponse(digits, request.url, getRoutingConfig());
	return new Response(responseXml, {
		headers: noStoreHeaders(),
	});
}
