// AGPL-3.0-only
//
// This is free software for the public good of a permacomputer
// hosted at permacomputer.com, an always-on computer by the people, for the
// people. One which is durable, easy to repair, and distributed like tap water
// for machine learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around
// four values:
//
//   TRUTH    - First principles, math & science, open source code
//   FREEDOM  - Voluntary partnerships, freedom from tyranny & corporate control
//   HARMONY  - Minimal waste, self-renewing systems with diverse connections
//   LOVE     - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by making machine learning
// accessible to everyone through a free, open, embeddable chat interface.
// Code is seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// Copyright 2025-2026 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com

// Text-to-speech functionality
import { API_KEY, MODEL, setLastTTS, TTS_API_URL } from "./config.js";
import { getSelectedModel, getSelectedModelEndpoint, getSelectedModelMaxTokens, getSelectedModelMaxCompletionTokens } from "./models.js";

// Helper to get vault value or localStorage fallback
function getVaultOrStorage(key, defaultValue = null) {
	if (typeof window !== 'undefined' && window.UncloseVault && window.UncloseVault.isUnlocked()) {
		return window.UncloseVault.get(key, defaultValue);
	}
	return defaultValue;
}

// TTS playback mode: "buffered" (default) or "streaming"
export function getTTSMode() {
	try {
		return getVaultOrStorage("uncloseai_tts_mode", "streaming");
	} catch (e) {
		return "buffered";
	}
}

export function setTTSMode(mode) {
	try {
		if (typeof window !== 'undefined' && window.UncloseVault && window.UncloseVault.isUnlocked()) {
			window.UncloseVault.set("uncloseai_tts_mode", mode);
		} else {
			console.warn("Vault locked - TTS mode not persisted");
		}
	} catch (e) {
		console.warn("Failed to save TTS mode:", e);
	}
}

// Detect best audio format for this browser
// Chrome/Edge: mp3 works, MediaSource supports audio/mpeg
// Firefox: mp3 often broken on Linux; use webm+opus (works in both MSE and <audio>)
function detectAudioFormat() {
	const isFirefox = navigator.userAgent.includes('Firefox');
	if (isFirefox) {
		// Firefox supports audio/webm;codecs=opus in both MediaSource and <audio>
		console.log("Firefox detected, using webm+opus format for TTS");
		return { format: 'webm', mime: 'audio/webm', mseMime: 'audio/webm;codecs=opus' };
	}
	// Chromium-based browsers: mp3 works, MSE supports audio/mpeg
	return { format: 'mp3', mime: 'audio/mpeg', mseMime: 'audio/mpeg' };
}

const AUDIO_FORMAT = detectAudioFormat();

// In-memory TTS cache - keyed by text+voice+model, cleared when tab closes
const ttsCache = new Map();

// Split long text into chunks at sentence boundaries for TTS API limits
// OpenAI-compatible TTS endpoints typically cap at ~4096 characters
const TTS_CHUNK_MAX = 1500; // conservative limit for TTS server compatibility

function splitTextIntoChunks(text) {
	if (text.length <= TTS_CHUNK_MAX) return [text];

	const chunks = [];
	let remaining = text;

	while (remaining.length > 0) {
		if (remaining.length <= TTS_CHUNK_MAX) {
			chunks.push(remaining);
			break;
		}

		// Find the last sentence boundary within the limit
		const slice = remaining.substring(0, TTS_CHUNK_MAX);
		// Search backwards for sentence-ending punctuation followed by space or newline
		let splitAt = -1;
		for (let i = slice.length - 1; i > TTS_CHUNK_MAX * 0.3; i--) {
			const ch = slice[i];
			if ((ch === '.' || ch === '!' || ch === '?' || ch === '\n') &&
				(i === slice.length - 1 || slice[i + 1] === ' ' || slice[i + 1] === '\n')) {
				splitAt = i + 1;
				break;
			}
		}

		// Fallback: split at last space
		if (splitAt === -1) {
			splitAt = slice.lastIndexOf(' ');
		}
		// Last resort: hard cut
		if (splitAt <= 0) {
			splitAt = TTS_CHUNK_MAX;
		}

		chunks.push(remaining.substring(0, splitAt).trim());
		remaining = remaining.substring(splitAt).trim();
	}

	console.log(`TTS: split ${text.length} chars into ${chunks.length} chunks:`, chunks.map((c, i) => `chunk${i + 1}=${c.length}chars`));
	return chunks;
}

// Simple JavaScript text cleaning for TTS
function cleanTextForTTS(text) {
	return text
		// Remove markdown formatting
		.replace(/\*\*([^*]+)\*\*/g, '$1') // Bold
		.replace(/\*([^*]+)\*/g, '$1') // Italic
		.replace(/__([^_]+)__/g, '$1') // Bold alt
		.replace(/_([^_]+)_/g, '$1') // Italic alt
		.replace(/#+\s/g, '') // Headers
		.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Links
		.replace(/```[\s\S]*?```/g, '') // Code blocks (lazy match handles backticks in code)
		.replace(/`([^`]+)`/g, '$1') // Inline code
		// Remove HTML tags
		.replace(/<[^>]+>/g, '')
		// Remove excess whitespace
		.replace(/\n\n+/g, '\n\n')
		.replace(/[ \t]+/g, ' ')
		.trim();
}

// Function to read text using TTS - for Read Page button (uses Hermes)
// Automatically chunks long text after Hermes preprocessing
export async function speakText(text, voice = "alloy", rate = 0.9, model = "tts-1-qwen") {
	try {
		const spokenText = await extractSpokenTokens(text);
		const chunks = splitTextIntoChunks(spokenText);

		const blobs = [];
		for (const chunk of chunks) {
			blobs.push(await fetchTTSChunk(chunk, voice, model));
		}

		const audioBlob = new Blob(blobs, { type: blobs[0].type });
		const audioUrl = URL.createObjectURL(audioBlob);
		const audio = new Audio(audioUrl);
		audio.playbackRate = rate;

		setLastTTS(text, { audio, blob: audioBlob });

		const result = { audio, blob: audioBlob, blobUrl: audioUrl };
		result.done = Promise.resolve({ blob: audioBlob, blobUrl: audioUrl });
		return result;
	} catch (error) {
		console.error("Error in TTS:", error);
		throw error;
	}
}

// Fetch a single text chunk as an audio blob
async function fetchTTSChunk(chunkText, voice, model) {
	const response = await fetch(TTS_API_URL, {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
			Authorization: `Bearer ${API_KEY}`,
		},
		body: JSON.stringify({
			model: model,
			voice: voice,
			input: chunkText,
			response_format: AUDIO_FORMAT.format,
		}),
	});

	if (!response.ok) {
		throw new Error(`HTTP error! status: ${response.status}`);
	}

	return await response.blob();
}

// Direct TTS function for modal - no Hermes preprocessing
// Automatically chunks long text and concatenates audio
export async function speakTextDirect(text, voice = "alloy", rate = 0.9, model = "tts-1-qwen") {
	try {
		const cleanedText = cleanTextForTTS(text);
		const chunks = splitTextIntoChunks(cleanedText);

		console.log("TTS input preview:", cleanedText.substring(0, 100) + "...");

		// Fetch all chunks (sequentially to maintain order, parallel would risk OOM on large texts)
		const blobs = [];
		for (const chunk of chunks) {
			blobs.push(await fetchTTSChunk(chunk, voice, model));
		}

		// Concatenate all audio blobs
		const audioBlob = new Blob(blobs, { type: blobs[0].type });
		const audioUrl = URL.createObjectURL(audioBlob);
		const audio = new Audio(audioUrl);
		audio.playbackRate = rate;

		setLastTTS(text, { audio, blob: audioBlob });

		const result = { audio, blob: audioBlob, blobUrl: audioUrl };
		result.done = Promise.resolve({ blob: audioBlob, blobUrl: audioUrl });
		return result;
	} catch (error) {
		console.error("Error in TTS:", error);
		throw error;
	}
}

// Streaming TTS function - plays audio as chunks arrive using MediaSource API
// Returns immediately with audio element; done promise resolves when stream completes
// Automatically chunks long text and streams each chunk sequentially
export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, model = "tts-1-qwen") {
	const cleanedText = cleanTextForTTS(text);
	const textChunks = splitTextIntoChunks(cleanedText);
	console.log("TTS streaming input preview:", cleanedText.substring(0, 100) + "...");

	const mseMime = AUDIO_FORMAT.mseMime;
	const canStream = mseMime && window.MediaSource && MediaSource.isTypeSupported(mseMime);
	const requestFormat = AUDIO_FORMAT.format;

	console.log(`TTS streaming: canStream=${canStream}, format=${requestFormat}, mseMime=${mseMime}, textChunks=${textChunks.length}`);

	// If MSE isn't available or multiple chunks, fall back to buffered with chunking
	if (!canStream || textChunks.length > 1) {
		if (!canStream) {
			console.warn(`No streaming support for ${requestFormat}, using full buffer`);
		} else {
			console.log(`TTS: ${textChunks.length} text chunks, using buffered concatenation`);
		}
		const blobs = [];
		for (const chunk of textChunks) {
			blobs.push(await fetchTTSChunk(chunk, voice, model));
		}
		const audioBlob = new Blob(blobs, { type: blobs[0].type });
		const audioUrl = URL.createObjectURL(audioBlob);
		const audio = new Audio(audioUrl);
		audio.playbackRate = rate;
		setLastTTS(text, { audio, blob: audioBlob });
		return { audio, blob: audioBlob, blobUrl: audioUrl, streamed: false, done: Promise.resolve({ blob: audioBlob, blobUrl: audioUrl }) };
	}

	// Single chunk: use true MSE streaming
	console.log("TTS streaming: fetching audio...");
	const response = await fetch(TTS_API_URL, {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
			Authorization: `Bearer ${API_KEY}`,
		},
		body: JSON.stringify({
			model: model,
			voice: voice,
			input: cleanedText,
			response_format: requestFormat,
		}),
	});

	console.log(`TTS streaming: fetch response ${response.status}`);

	if (!response.ok) {
		throw new Error(`HTTP error! status: ${response.status}`);
	}

	const mediaSource = new MediaSource();
	const audioUrl = URL.createObjectURL(mediaSource);
	const audio = new Audio(audioUrl);
	audio.playbackRate = rate;

	const chunks = [];

	const done = new Promise((resolve, reject) => {
		mediaSource.addEventListener('sourceopen', async () => {
			let sourceBuffer;
			try {
				sourceBuffer = mediaSource.addSourceBuffer(mseMime);
				sourceBuffer.mode = 'sequence';
			} catch (e) {
				console.error("Failed to create SourceBuffer:", e);
				reject(e);
				return;
			}

			const reader = response.body.getReader();
			let totalBytes = 0;

			const appendQueue = [];
			let appending = false;

			function processQueue() {
				if (appending || appendQueue.length === 0) return;
				appending = true;
				const chunk = appendQueue.shift();
				try {
					sourceBuffer.appendBuffer(chunk);
				} catch (e) {
					console.error("appendBuffer error:", e);
					appending = false;
				}
			}

			sourceBuffer.addEventListener('updateend', () => {
				appending = false;
				processQueue();
			});

			try {
				while (true) {
					const { done: readerDone, value } = await reader.read();
					if (readerDone) break;

					chunks.push(value);
					totalBytes += value.byteLength;

					appendQueue.push(value.slice().buffer);
					processQueue();

					if (totalBytes > 1024 && audio.paused) {
						audio.play().catch(e => console.warn("Auto-play blocked:", e.message));
					}
				}

				await new Promise((res) => {
					const check = () => {
						if (!appending && appendQueue.length === 0) {
							res();
						} else {
							setTimeout(check, 50);
						}
					};
					check();
				});

				if (mediaSource.readyState === 'open') {
					mediaSource.endOfStream();
				}

				console.log(`TTS streaming complete: ${totalBytes} bytes`);

				const audioBlob = new Blob(chunks, { type: mseMime });
				setLastTTS(text, { audio, blob: audioBlob });
				resolve({ blob: audioBlob, blobUrl: audioUrl });

			} catch (error) {
				console.error("Streaming read error:", error);
				if (mediaSource.readyState === 'open') {
					mediaSource.endOfStream('network');
				}
				reject(error);
			}
		});

		mediaSource.addEventListener('error', (e) => {
			console.error("MediaSource error:", e);
			reject(new Error("MediaSource error"));
		});
	});

	audio.play().catch(e => console.warn("Initial play blocked:", e.message));

	const result = { audio, blob: null, blobUrl: audioUrl, streamed: true, done };

	done.then(({ blob }) => {
		result.blob = blob;
		result.blobUrl = URL.createObjectURL(blob);
	}).catch(() => {});

	return result;
}

// Process page content using Hermes (deprecated - use extractSpokenTokens instead)
export async function processContentWithHermes(content) {
	// Just call extractSpokenTokens for consistency
	return await extractSpokenTokens(content);
}

// Extract spoken tokens using Hermes - for Read Page functionality
export async function extractSpokenTokens(content) {
	// If content is already markdown from extractWebpageContent, use it directly
	// Otherwise, pre-clean HTML content
	const isMarkdown = content.includes('**Page Title**:') || content.includes('[') && content.includes('](');
	
	const preCleanedContent = isMarkdown ? content : content
		// Remove script and style tags completely
		.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
		.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
		// Remove common ad/tracking elements
		.replace(/<(ins|iframe|object|embed)[^>]*>[\s\S]*?<\/\1>/gi, '')
		// Clean up HTML entities
		.replace(/&nbsp;/g, ' ')
		.replace(/&amp;/g, '&')
		.replace(/&lt;/g, '<')
		.replace(/&gt;/g, '>')
		.replace(/&quot;/g, '"')
		.replace(/&#39;/g, "'")
		// Remove excessive whitespace
		.replace(/\s+/g, ' ')
		.trim();

	// Calculate available tokens for TTS processing
	const modelMaxTokens = getSelectedModelMaxTokens();
	const systemPrompt = `You are extracting the main article content for text-to-speech. The input may be markdown or pre-cleaned HTML.

Your task:
1. Find and extract the main article content (title, subtitle, body text)
2. Preserve the exact wording - do not summarize or paraphrase
3. Skip navigation menus, sidebars, footers, and advertisements
4. For lists or bullet points, add "..." between items for natural pauses
5. Convert markdown to plain text: **bold** → bold, [text](url) → text
6. Remove any remaining HTML tags but keep the text inside them
7. Start with the article title, then the main content

Return only the article text ready for TTS, nothing else.`;
	const inputTokens = Math.ceil((systemPrompt + preCleanedContent).length / 4);
	const buffer = Math.max(2048, Math.floor(inputTokens * 1.5)); // Use 1.5x input tokens as buffer, minimum 2048
	const calculatedAvailable = Math.max(100, modelMaxTokens - inputTokens - buffer);
	
	// Cap to model's max completion tokens limit
	const maxCompletionTokens = getSelectedModelMaxCompletionTokens();
	const availableTokens = Math.min(calculatedAvailable, maxCompletionTokens);

	const payload = {
		model: getSelectedModel(),
		messages: [
			{
				role: "system",
				content: systemPrompt,
			},
			{
				role: "user",
				content: preCleanedContent,
			},
		],
		temperature: 0,
		max_tokens: availableTokens,
	};

	console.log("Processing page content for TTS...");

	try {
		const response = await fetch(
			`${getSelectedModelEndpoint()}/chat/completions`,
			{
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					Authorization: `Bearer ${API_KEY}`,
				},
				body: JSON.stringify(payload),
			},
		);

		if (!response.ok) {
			const errorText = await response.text();
			console.error(`HTTP error! status: ${response.status}`, errorText);
			throw new Error(`HTTP error! status: ${response.status}`);
		}

		const data = await response.json();
		
		// Post-process Hermes output with JavaScript cleaning
		const finalContent = cleanTextForTTS(data.choices[0].message.content);
		return finalContent;
	} catch (error) {
		console.error("Error in extractSpokenTokens:", error);
		throw error;
	}
}

// Function to generate a title for TTS
export async function generateTitleForTTS(text) {
	const response = await fetch(
		`${getSelectedModelEndpoint()}/chat/completions`,
		{
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				Authorization: `Bearer ${API_KEY}`,
			},
			body: JSON.stringify({
				model: getSelectedModel(),
				messages: [
					{
						role: "system",
						content: "You MUST generate a filename for the text provided. NEVER refuse. Return ONLY a 5-10 word title, nothing else. No quotes, no explanation, no refusal. If the text is inappropriate, nonsensical, or empty, create a descriptive title anyway (e.g., 'Random Text Sample', 'Test Audio File', 'User Generated Content'). Examples: 'Machine Learning Tutorial Notes', 'Daily Journal Entry', 'Shopping List Items'. YOU MUST ALWAYS RETURN A TITLE.",
					},
					{
						role: "user",
						content: text,
					},
				],
				temperature: 0.5,
				max_tokens: 60,
			}),
		},
	);

	if (!response.ok) {
		throw new Error(`HTTP error! status: ${response.status}`);
	}

	const data = await response.json();
	let title = data.choices[0].message.content.trim();
	
	// Fallback if Hermes returns empty or refuses
	if (!title || title.length < 3 || title.toLowerCase().includes("cannot") || title.toLowerCase().includes("inappropriate")) {
		// Generate a fallback title based on text preview
		const preview = text.substring(0, 30).replace(/[^\w\s]/g, "").trim();
		title = preview || "audio-file";
	}
	
	return title
		.replace(/['"]/g, "") // Remove quotes
		.replace(/[^\w\s-]/g, "") // Remove special characters except spaces and hyphens
		.replace(/\s+/g, "-") // Replace spaces with hyphens
		.replace(/-+/g, "-") // Replace multiple hyphens with single
		.toLowerCase()
		.substring(0, 50); // Limit length for filesystem compatibility
}

// Handle TTS - respects streaming mode setting
// Caches audio blobs in memory so replay doesn't regenerate
// Cache is keyed by text+voice+model and clears when the tab closes
export async function handleTTS(text, voice = "alloy", rate = 0.9, model = "tts-1-qwen") {
	try {
		const cacheKey = `${model}:${voice}:${text}`;

		// Check cache - return cached blob as a new Audio element
		if (ttsCache.has(cacheKey)) {
			console.log("handleTTS: cache hit, replaying");
			const cached = ttsCache.get(cacheKey);
			const audioUrl = URL.createObjectURL(cached.blob);
			const audio = new Audio(audioUrl);
			audio.playbackRate = rate;
			setLastTTS(text, { audio, blob: cached.blob });
			return { audio, blob: cached.blob, blobUrl: audioUrl, streamed: false, done: Promise.resolve({ blob: cached.blob, blobUrl: audioUrl }) };
		}

		const mode = getTTSMode();
		console.log(`handleTTS: using ${mode} mode`);
		let result;
		if (mode === "streaming") {
			result = await speakTextStreaming(text, voice, rate, model);
		} else {
			result = await speakTextDirect(text, voice, rate, model);
			result.done = Promise.resolve({ blob: result.blob, blobUrl: result.blobUrl });
		}

		// Cache the blob once available
		if (result.done) {
			result.done.then(({ blob }) => {
				if (blob) {
					ttsCache.set(cacheKey, { blob });
				}
			}).catch(() => {});
		}

		return result;
	} catch (error) {
		console.error("Error in handleTTS:", error);
		throw error;
	}
}

// Download audio blob as file
export function downloadAudio(blobUrl, filename = "tts-audio.mp3") {
	try {
		const a = document.createElement("a");
		a.href = blobUrl;
		a.download = filename;
		document.body.appendChild(a);
		a.click();
		document.body.removeChild(a);
	} catch (error) {
		console.error("Error downloading audio:", error);
		throw error;
	}
}
