import { JsonCleaner } from "../utils/jsonCleaner";
import { get, post } from "./serviceHelper";
function getExecutionResultMessage(response, fallback) {
    const parts = [response.summary, response.details]
        .map((part) => part?.trim())
        .filter((part) => !!part);
    return parts.length > 0 ? parts.join(": ") : fallback;
}
export function getAiProfilesErrorMessage(error) {
    if (error instanceof Error && error.message.trim()) {
        return error.message.trim();
    }
    return "Failed to load agent profiles.";
}
// In-flight de-duplication and short TTL cache for AI profiles to avoid
// duplicate network requests in React StrictMode and across multiple callers.
const aiProfilesInFlight = new Map();
const aiProfilesCache = new Map();
const AI_PROFILES_TTL_MS = 60_000; // 1 minute
// Cache invalidation epoch. Bumped whenever profiles change via websocket.
let aiProfilesEpoch = 0;
function makeProfilesCacheKey(item) {
    if (!item)
        return "global";
    const id = item?.id || "";
    const language = item?.language ?? "";
    const version = item?.version ?? "";
    return `${id}:${language}:${version}`;
}
export async function loadAiProfiles(item) {
    const url = "/parhelia/agent/profiles";
    const cacheKey = makeProfilesCacheKey(item);
    // Serve from cache if fresh
    const cached = aiProfilesCache.get(cacheKey);
    if (cached &&
        cached.epoch === aiProfilesEpoch &&
        Date.now() - cached.timestamp < AI_PROFILES_TTL_MS) {
        return cached.data;
    }
    // Reuse in-flight request for the same key
    const inFlight = aiProfilesInFlight.get(cacheKey);
    if (inFlight)
        return inFlight;
    const requestPromise = (async () => {
        const epochAtStart = aiProfilesEpoch;
        const response = await post(url, item ?? {});
        if (response.type !== "success") {
            throw new Error(getExecutionResultMessage(response, "Failed to load agent profiles."));
        }
        const data = response.data || [];
        // Only populate cache if no invalidation happened while request was in-flight
        if (aiProfilesEpoch === epochAtStart) {
            aiProfilesCache.set(cacheKey, {
                timestamp: Date.now(),
                data,
                epoch: epochAtStart,
            });
        }
        return data;
    })().finally(() => {
        aiProfilesInFlight.delete(cacheKey);
    });
    aiProfilesInFlight.set(cacheKey, requestPromise);
    return requestPromise;
}
/**
 * Invalidates cached AI profiles so the next `loadAiProfiles` refetches immediately.
 * Call this when profiles are changed (e.g. via websocket).
 */
export function invalidateAiProfilesCache() {
    aiProfilesEpoch++;
    aiProfilesCache.clear();
    aiProfilesInFlight.clear();
}
// Cache for help topics
let helpTopicsCache = null;
let helpTopicsInFlight = null;
const HELP_TOPICS_TTL_MS = 60_000; // 1 minute
export async function loadHelpTopics() {
    const url = "/parhelia/helptopics";
    // Serve from cache if fresh
    if (helpTopicsCache &&
        Date.now() - helpTopicsCache.timestamp < HELP_TOPICS_TTL_MS) {
        return helpTopicsCache.data;
    }
    // Reuse in-flight request
    if (helpTopicsInFlight)
        return helpTopicsInFlight;
    helpTopicsInFlight = (async () => {
        const response = await get(url);
        if (response.type !== "success") {
            throw new Error("Error loading help topics");
        }
        const data = response.data || [];
        helpTopicsCache = { timestamp: Date.now(), data };
        return data;
    })().finally(() => {
        helpTopicsInFlight = null;
    });
    return helpTopicsInFlight;
}
// Unified function signature
export async function executePrompt(messages, context, options = {}, requestOptions, callback, stream) {
    let requestBody;
    let finalRequestOptions;
    // Handle context - either direct AiContext or need to create it
    let aiContext;
    if ("promptData" in context) {
        // Direct AiContext (AiTerminal style)
        aiContext = context;
    }
    else {
        // Create context (other callers style)
        aiContext = context.createAiContext({ editContext: context.editContext });
        // Default sessionId from editContext if not provided
        if (options.sessionId === undefined) {
            options.sessionId = context.editContext.sessionId;
        }
    }
    // Build unified request body
    requestBody = {
        messages,
        ...aiContext.promptData,
        sessionId: options.sessionId,
        model: options.model,
        stream: stream,
        // Include all provided options
        ...options,
    };
    finalRequestOptions = {
        method: "POST",
        body: JSON.stringify(requestBody),
        credentials: "include",
        headers: {
            "Content-Type": "application/json",
        },
        ...requestOptions,
    };
    // Use agent endpoint; page-wizard package may override via options.endpoint or aiContext.endpoint
    const endpoint = options.endpoint || aiContext.endpoint || "/parhelia/agent/prompt";
    const response = await fetch(endpoint, finalRequestOptions);
    if (!response.ok) {
        // Always return rich error format
        const text = await response.text();
        const errorContent = "There was an error processing your request: " + text;
        return {
            messages: [
                {
                    content: errorContent,
                    name: "assistant",
                    role: "assistant",
                    id: crypto.randomUUID(),
                    tool_calls: [],
                },
            ],
            content: errorContent,
            editOperations: [],
            numInputTokens: 0,
            numOutputTokens: 0,
            numCachedTokens: 0,
            state: "error",
        };
    }
    if (!response?.body)
        return null;
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let result = null;
    let accumulatedContent = "";
    while (true) {
        const { done, value } = await reader.read();
        if (done) {
            break;
        }
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        if (lines.length > 0) {
            buffer = lines.pop() || "";
            for (let line of lines) {
                if (line.trim() === "")
                    continue;
                try {
                    const jsonData = JSON.parse(line);
                    if (jsonData.isIncremental) {
                        if (jsonData.deltaContent) {
                            accumulatedContent += jsonData.deltaContent;
                        }
                        // Always apply JsonCleaner for streaming responses
                        const cleanupResult = JsonCleaner.cleanupJson(accumulatedContent);
                        const incrementalResponse = {
                            ...jsonData,
                            messages: [
                                {
                                    content: cleanupResult.cleanedJson,
                                    role: "assistant",
                                    name: "assistant",
                                    id: Date.now(),
                                    tool_calls: [],
                                },
                            ],
                        };
                        callback?.(incrementalResponse);
                        result = incrementalResponse;
                    }
                    else {
                        // Always ensure messages have IDs and tool_calls
                        let processedResponse = jsonData;
                        if (jsonData.messages) {
                            processedResponse = {
                                ...jsonData,
                                messages: jsonData.messages.map((msg, index) => ({
                                    ...msg,
                                    id: msg.id || Date.now() + index,
                                    tool_calls: msg.tool_calls || [],
                                })),
                            };
                        }
                        callback?.(processedResponse);
                        result = processedResponse;
                    }
                }
                catch (e) {
                    console.error("Error parsing line:" + line, e);
                }
            }
        }
    }
    // Handle final buffer
    if (buffer.trim() !== "") {
        try {
            const jsonData = JSON.parse(buffer);
            if (jsonData.isIncremental) {
                if (jsonData.deltaContent) {
                    accumulatedContent += jsonData.deltaContent;
                }
                const cleanupResult = JsonCleaner.cleanupJson(accumulatedContent);
                result = {
                    ...jsonData,
                    messages: [
                        {
                            content: cleanupResult.cleanedJson,
                            role: "assistant",
                            name: "assistant",
                            id: Date.now(),
                            tool_calls: [],
                        },
                    ],
                };
            }
            else {
                // Always ensure messages have IDs
                if (jsonData.messages) {
                    result = {
                        ...jsonData,
                        messages: jsonData.messages.map((msg, index) => ({
                            ...msg,
                            id: msg.id || Date.now() + index,
                            tool_calls: msg.tool_calls || [],
                        })),
                    };
                }
                else {
                    result = jsonData;
                }
            }
        }
        catch (e) {
            console.error("Error parsing the final buffer content:", e);
        }
    }
    return result;
}
/**
 * Helper function to parse JSON content from AI response
 * Strips markdown code blocks if present and parses the JSON
 * Handles incomplete markdown blocks during streaming
 */
function parseJsonContent(content) {
    let cleanedContent = content;
    // Strip markdown code blocks if present
    if (cleanedContent.startsWith("```json")) {
        // Remove the opening ```json
        cleanedContent = cleanedContent.substring(7).trim();
        // Find the closing ``` and extract only content between the markers
        const closingIndex = cleanedContent.indexOf("```");
        if (closingIndex !== -1) {
            cleanedContent = cleanedContent.substring(0, closingIndex).trim();
        }
    }
    else if (cleanedContent.startsWith("```")) {
        // Remove the opening ```
        cleanedContent = cleanedContent.substring(3).trim();
        // Find the closing ``` and extract only content between the markers
        const closingIndex = cleanedContent.indexOf("```");
        if (closingIndex !== -1) {
            cleanedContent = cleanedContent.substring(0, closingIndex).trim();
        }
    }
    // Replace JavaScript undefined with JSON null
    // Pattern matches: undefined as a value (not inside strings)
    cleanedContent = cleanedContent.replace(/:\s*undefined\s*([,\}])/g, ": null$1");
    const tryParse = (jsonText) => {
        try {
            return JSON.parse(jsonText);
        }
        catch {
            return undefined;
        }
    };
    const directParse = tryParse(cleanedContent);
    if (directParse !== undefined) {
        return directParse;
    }
    const cleanedParse = tryParse(JsonCleaner.cleanupJson(cleanedContent).cleanedJson);
    if (cleanedParse !== undefined) {
        return cleanedParse;
    }
    const extractedJson = extractFirstJsonValue(cleanedContent);
    if (extractedJson) {
        const extractedParse = tryParse(JsonCleaner.cleanupJson(extractedJson).cleanedJson);
        if (extractedParse !== undefined) {
            return extractedParse;
        }
    }
    throw new Error("Failed to parse AI JSON response");
}
function extractFirstJsonValue(content) {
    const startIndex = content.search(/[\{\[]/);
    if (startIndex === -1)
        return null;
    let depth = 0;
    let inString = false;
    let isEscape = false;
    for (let i = startIndex; i < content.length; i += 1) {
        const char = content[i];
        if (char === '"' && !isEscape) {
            inString = !inString;
        }
        else if (!inString) {
            if (char === "{" || char === "[") {
                depth += 1;
            }
            else if (char === "}" || char === "]") {
                depth -= 1;
                if (depth === 0) {
                    return content.slice(startIndex, i + 1);
                }
            }
        }
        isEscape = inString && char === "\\" && !isEscape;
        if (char !== "\\") {
            isEscape = false;
        }
    }
    return content.slice(startIndex);
}
/**
 * Executes a prompt and returns the parsed JSON result
 * @param messages - The messages to send
 * @param context - The AI context
 * @param options - Execution options
 * @param requestOptions - Additional request options
 * @param callback - Optional callback for streaming responses
 * @param stream - Whether to stream the response
 * @returns The parsed JSON object from the response content
 * @throws Error if the response cannot be parsed or is not in the expected format
 */
export async function executePromptWithJsonResult(messages, context, options = {}, requestOptions, callback, stream) {
    // Wrap the callback to parse JSON before passing to the original callback
    const wrappedCallback = callback
        ? (response) => {
            if (!response || !response.content) {
                return;
            }
            try {
                const parsed = parseJsonContent(response.content);
                callback(parsed);
            }
            catch (parseError) {
                // During streaming, JSON may still be incomplete even after cleaning
                // Only invoke callback when we have valid parseable JSON
            }
        }
        : undefined;
    const result = await executePrompt(messages, context, options, requestOptions, wrappedCallback, stream);
    if (!result || !result.content) {
        throw new Error("No content in response");
    }
    try {
        return parseJsonContent(result.content);
    }
    catch (parseError) {
        throw new Error(`Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
    }
}
// moved to searchService.ts
export async function generateImage(options) {
    const response = await post("/parhelia/page-wizard/generateImage", options);
    return response;
}
export async function requestQuota(options = {}) {
    const params = new URLSearchParams();
    if (options.scope)
        params.set("scope", options.scope);
    if (options.userName)
        params.set("userName", options.userName);
    const query = params.toString();
    const url = `/parhelia/agent/requestQuota${query ? `?${query}` : ""}`;
    return get(url);
}
//# sourceMappingURL=aiService.js.map