/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const localSessions = new Map();
/**
 * SSRF guard: only allow requests to loopback addresses.
 */
export function isLocalEndpoint(endpoint) {
    try {
        const url = new URL(endpoint);
        return ['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname);
    }
    catch {
        return false;
    }
}
/**
 * Create a new local LLM streaming session.
 * Sends POST to the OpenAI-compatible chat/completions endpoint with `stream: true`
 * and parses SSE responses.
 */
export function createLocalLlmSession(sessionId, message, config, context) {
    if (!isLocalEndpoint(config.endpoint)) {
        const session = {
            id: sessionId,
            abortController: new AbortController(),
            outputBuffer: '',
            history: [{ role: 'user', content: message }],
            status: 'error',
        };
        localSessions.set(sessionId, session);
        // Fire error callback asynchronously
        queueMicrotask(() => session.onError?.('Endpoint must be a local address (localhost/127.0.0.1/::1)'));
        return session;
    }
    const abortController = new AbortController();
    const session = {
        id: sessionId,
        abortController,
        outputBuffer: '',
        history: [{ role: 'user', content: message }],
        status: 'streaming',
    };
    localSessions.set(sessionId, session);
    const messages = [];
    if (context) {
        messages.push({ role: 'system', content: context });
    }
    // Include history from previous turns if any
    for (const entry of session.history) {
        messages.push({ role: entry.role, content: entry.content });
    }
    const url = `${config.endpoint}/chat/completions`;
    void streamCompletion(session, url, config.model, messages, abortController.signal);
    return session;
}
async function streamCompletion(session, url, model, messages, signal) {
    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ model, messages, stream: true }),
            signal,
        });
        if (!response.ok) {
            const text = await response.text().catch(() => '');
            session.status = 'error';
            session.onError?.(`Local LLM returned HTTP ${String(response.status)}: ${text.slice(0, 200)}`);
            return;
        }
        const reader = response.body?.getReader();
        if (!reader) {
            session.status = 'error';
            session.onError?.('No response body from local LLM');
            return;
        }
        const decoder = new TextDecoder();
        let buffer = '';
        // eslint-disable-next-line no-constant-condition, no-await-in-loop
        for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
            const value = chunk.value;
            if (!value)
                continue;
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            // Keep the last incomplete line in the buffer
            buffer = lines.pop() ?? '';
            for (const line of lines) {
                const trimmed = line.trim();
                if (!trimmed || trimmed.startsWith(':'))
                    continue;
                if (!trimmed.startsWith('data: '))
                    continue;
                const payload = trimmed.slice(6);
                if (payload === '[DONE]') {
                    session.status = 'done';
                    session.history.push({ role: 'assistant', content: session.outputBuffer });
                    session.onDone?.();
                    return;
                }
                try {
                    const parsed = JSON.parse(payload);
                    const content = parsed.choices?.[0]?.delta?.content;
                    if (content) {
                        session.outputBuffer += content;
                        session.onToken?.(content);
                    }
                }
                catch {
                    // Skip malformed JSON chunks
                }
            }
        }
        // Stream ended without [DONE] — treat as complete
        if (session.status === 'streaming') {
            session.status = 'done';
            session.history.push({ role: 'assistant', content: session.outputBuffer });
            session.onDone?.();
        }
    }
    catch (err) {
        if (signal.aborted)
            return;
        session.status = 'error';
        const msg = err instanceof Error ? err.message : 'Unknown streaming error';
        session.onError?.(msg);
    }
}
export function getLocalLlmSession(sessionId) {
    return localSessions.get(sessionId);
}
export function abortLocalLlmSession(sessionId) {
    const session = localSessions.get(sessionId);
    if (!session)
        return false;
    session.abortController.abort();
    session.status = 'error';
    localSessions.delete(sessionId);
    return true;
}
//# sourceMappingURL=local-llm-adapter.js.map