// 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

// Chat functionality and message handling
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/highlight.min.js";
import { API_KEY, getAPIConfig } from "./config.js";
import { getSelectedModel, getSelectedModelEndpoint, getSelectedModelMaxTokens, getSelectedModelMaxCompletionTokens } from "./models.js";
import { groqAwareStreamingFetch } from "./groq-fetch.js";

// Rough token estimation (4 chars = 1 token average for English)
function estimateTokens(text) {
	return Math.ceil(text.length / 4);
}

// Calculate available output tokens
function calculateAvailableTokens(chatHistory, maxTokens) {
	const inputText = chatHistory.map(msg => msg.content).join('');
	const inputTokens = estimateTokens(inputText);
	
	// For large context models (>32k), use a more reasonable buffer
	// For smaller models, be more conservative
	let buffer;
	if (maxTokens > 32000) {
		// Large context models: just reserve 10% or 2k tokens for safety
		buffer = Math.min(2000, Math.floor(maxTokens * 0.1));
	} else if (maxTokens > 8000) {
		// Medium models: reserve 20% or 1.5k tokens
		buffer = Math.min(1500, Math.floor(maxTokens * 0.2));
	} else {
		// Small models: keep conservative approach
		buffer = Math.max(1000, Math.floor(inputTokens * 0.5));
	}
	
	const availableTokens = Math.max(2000, maxTokens - inputTokens - buffer);
	console.log(`Token calculation: max=${maxTokens}, input≈${inputTokens}, buffer=${buffer}, available≈${availableTokens}`);
	return availableTokens;
}
import { initializeChatHistory, saveConversationHistory, getCustomPromptEcho } from "./storage.js";
import { generateTitleForTTS, speakText } from "./tts.js";

// Helper function to add copy buttons to code blocks
function addCodeBlockCopyButtons(element) {
	const codeBlocks = element.querySelectorAll("pre code");
	codeBlocks.forEach((codeBlock) => {
		const pre = codeBlock.parentElement;
		if (pre.tagName.toLowerCase() === "pre") {
			// Check if copy button already exists to avoid duplicates
			if (pre.querySelector('.code-copy-btn')) {
				return;
			}

			// Make the pre element relative for positioning
			pre.style.position = "relative";

			// Create copy button
			const copyBtn = document.createElement("button");
			copyBtn.className = "uncloseai-code-copy-btn";
			copyBtn.textContent = "📋";
			copyBtn.title = "Copy code";
			// Styling handled by CSS class

			copyBtn.onclick = async () => {
				try {
					await navigator.clipboard.writeText(codeBlock.textContent);
					copyBtn.textContent = "✓";
					setTimeout(() => {
						copyBtn.textContent = "📋";
					}, 2000);
				} catch (error) {
					console.error("Failed to copy code:", error);
				}
			};

			pre.appendChild(copyBtn);
		}
	});
}

// Initialize chat history (will be populated on first use)
export let chatHistory = [];

// Generator function to send a message to the LLM and yield responses
export async function* sendMessage(message) {
	// Initialize chat history if empty
	if (chatHistory.length === 0) {
		chatHistory = await initializeChatHistory();
	}
	
	chatHistory.push({ role: "user", content: message });

	// Get API configuration (custom or default)
	const apiConfig = await getAPIConfig();
	
	let apiUrl, headers, model;
	
	if (apiConfig.isCustom) {
		// Use custom API configuration
		apiUrl = `${apiConfig.endpoint}/chat/completions`;
		headers = apiConfig.headers;
		model = apiConfig.model;
	} else {
		// Use default Hermes configuration
		apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
		headers = {
			"Content-Type": "application/json",
			Authorization: `Bearer ${API_KEY}`,
			"X-Uncloseai-Client": "browser-toy",
		};
		model = getSelectedModel();
	}

	// Build messages for LLM: chatHistory + echo (echo is only for LLM attention, not stored)
	const echo = getCustomPromptEcho();
	const messagesForLLM = echo
		? [...chatHistory, { role: "assistant", content: echo }]
		: chatHistory;

	const modelMaxTokens = getSelectedModelMaxTokens();
	const calculatedAvailable = calculateAvailableTokens(messagesForLLM, modelMaxTokens);

	// Cap to model's max completion tokens limit
	const maxCompletionTokens = getSelectedModelMaxCompletionTokens();
	// Use the full calculated available space or the model's limit, whichever is smaller
	const finalAvailable = Math.min(calculatedAvailable, maxCompletionTokens);

	console.log("Chat using tokens:", finalAvailable, "calculated:", calculatedAvailable, "max completion:", maxCompletionTokens);

	const requestBody = JSON.stringify({
		model: model,
		messages: messagesForLLM,
		temperature: 0.5,
		max_tokens: finalAvailable,
		stream: true,
	});

	const streamGenerator = groqAwareStreamingFetch(apiUrl, {
		method: "POST", 
		headers: headers,
		body: requestBody,
	});

	let buffer = "";

	for await (const chunk of streamGenerator) {
		buffer += chunk;
		const lines = buffer.split("\n");

		for (let i = 0; i < lines.length - 1; i++) {
			const line = lines[i].trim();
			if (line.startsWith("data: ")) {
				const jsonData = line.slice(6);
				if (jsonData === "[DONE]") continue;

				try {
					const parsedData = JSON.parse(jsonData);
					const content = parsedData.choices[0].delta.content;
					if (content) {
						yield content;
					}
				} catch (error) {
					console.error("Error parsing JSON:", error);
				}
			}
		}

		buffer = lines[lines.length - 1];
	}
}

// Function to handle user input and display responses
export async function handleUserInput() {
	const userInput = document.getElementById("user-input").value;
	document.getElementById("user-input").value = "";

	const chatBox = document.getElementById("chat-box");
	chatBox.innerHTML += `<p><strong>You:</strong> ${userInput}</p>`;

	const aiResponseParagraph = document.createElement("p");
	aiResponseParagraph.innerHTML = "<strong>AI:</strong> ";
	chatBox.appendChild(aiResponseParagraph);

	const responseContent = document.createElement("span");
	aiResponseParagraph.appendChild(responseContent);

	let accumulatedContent = "";
	for await (const chunk of sendMessage(userInput)) {
		accumulatedContent += chunk;
		const parsedChunk = marked.parse(accumulatedContent);
		responseContent.innerHTML = parsedChunk;

		responseContent.querySelectorAll("pre code").forEach((block) => {
			hljs.highlightElement(block);
		});
	}

	// Streaming is complete - add copy buttons to code blocks
	addCodeBlockCopyButtons(responseContent);

	chatBox.scrollTop = chatBox.scrollHeight;

	// Add the response to chat history
	chatHistory.push({ role: "assistant", content: accumulatedContent });
	saveConversationHistory(chatHistory);

	// Add play/pause button for TTS
	const playPauseButton = document.createElement("button");
	playPauseButton.textContent = "Generate TTS for AI Response";
	playPauseButton.className = "uncloseai-ui-button-margin";
	let aiAudio = null;
	let aiBlob = null;
	let isPaused = false;

	playPauseButton.onclick = async () => {
		if (!aiAudio) {
			playPauseButton.textContent = "Processing...";
			playPauseButton.disabled = true; // Disable button while processing
			const mainVoiceSelect = document.getElementById("read-page-voice");
			const selectedVoice = mainVoiceSelect ? mainVoiceSelect.value : "alloy";
			const result = await speakText(accumulatedContent, selectedVoice, 0.9);
			aiAudio = result.audio;
			aiBlob = result.blob;
			playPauseButton.textContent = "Pause AI Response";
			playPauseButton.disabled = false; // Re-enable button after processing
			aiAudio.play();

			// Generate title for the MP3 file
			const title = await generateTitleForTTS(accumulatedContent);

			// Add download button
			const downloadButton = document.createElement("button");
			downloadButton.textContent = "Download MP3";
			downloadButton.className = "uncloseai-ui-button-margin";
			downloadButton.onclick = () => {
				const a = document.createElement("a");
				a.href = URL.createObjectURL(aiBlob);
				a.download = `${title}.mp3`;
				a.click();
			};
			chatBox.appendChild(downloadButton);
		} else {
			if (isPaused) {
				aiAudio.play();
				playPauseButton.textContent = "Pause AI Response";
			} else {
				aiAudio.pause();
				playPauseButton.textContent = "Play AI Response";
			}
			isPaused = !isPaused;
		}
	};
	chatBox.appendChild(playPauseButton);
}

// Update chat history reference (for external modules)
export function updateChatHistory(newHistory) {
	chatHistory = newHistory;
}

export function getChatHistory() {
	return chatHistory;
}

// Generator function to send a message with custom history (doesn't modify global chatHistory)
export async function* sendMessageWithCustomHistory(customHistory) {
	// Get API configuration (custom or default)
	const apiConfig = await getAPIConfig();
	
	let apiUrl, headers, model;
	
	if (apiConfig.isCustom) {
		// Use custom API configuration
		apiUrl = `${apiConfig.endpoint}/chat/completions`;
		headers = apiConfig.headers;
		model = apiConfig.model;
	} else {
		// Use default Hermes configuration
		apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
		headers = {
			"Content-Type": "application/json",
			Authorization: `Bearer ${API_KEY}`,
			"X-Uncloseai-Client": "browser-toy",
		};
		model = getSelectedModel();
	}

	// Build messages for LLM: customHistory + echo (echo is only for LLM attention, not stored)
	const echo = getCustomPromptEcho();
	const messagesForLLM = echo
		? [...customHistory, { role: "assistant", content: echo }]
		: customHistory;

	const modelMaxTokens = getSelectedModelMaxTokens();
	const calculatedAvailable = calculateAvailableTokens(messagesForLLM, modelMaxTokens);

	// Cap to model's max completion tokens limit
	const maxCompletionTokens = getSelectedModelMaxCompletionTokens();
	const finalAvailable = Math.min(calculatedAvailable, maxCompletionTokens);
	console.log("Chat (custom history) using tokens:", finalAvailable, "calculated:", calculatedAvailable, "max completion:", maxCompletionTokens);

	const response = await fetch(apiUrl, {
		method: "POST",
		headers: headers,
		body: JSON.stringify({
			model: model,
			messages: messagesForLLM,
			max_tokens: finalAvailable,
			stream: true,
		}),
	});

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

	const reader = response.body.getReader();
	const decoder = new TextDecoder();

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

			const chunk = decoder.decode(value);
			const lines = chunk.split("\n");

			for (const line of lines) {
				if (line.startsWith("data: ")) {
					const data = line.slice(6);
					if (data === "[DONE]") {
						return;
					}

					try {
						const parsed = JSON.parse(data);
						const content = parsed.choices?.[0]?.delta?.content;
						if (content) {
							yield content;
						}
					} catch (error) {
						// Skip malformed JSON
					}
				}
			}
		}
	} finally {
		reader.releaseLock();
	}
}
