import { ModelProviderError } from '../errors.js';
import { ATOM_EXTRACTION_SYSTEM_PROMPT, ATOM_UTILITY_SYSTEM_PROMPT, atomExtractionSchema, atomUtilitySchema, parseAtomUtilityVerdict, parseExtractedAtoms, } from './atom-extraction.js';
const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
const DEFAULT_TIMEOUT_MS = 600_000;
export class OllamaGenerationProvider {
    name = 'ollama-generation';
    modelId;
    ollamaUrl;
    timeoutMs;
    fetchFn;
    constructor(options) {
        this.modelId = options.modelId;
        this.ollamaUrl = trimTrailingSlash(options.ollamaUrl ?? DEFAULT_OLLAMA_URL);
        this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
        this.fetchFn = options.fetchFn ?? globalThis.fetch;
    }
    async extractAtoms(input) {
        const content = await this.chatJson(ATOM_EXTRACTION_SYSTEM_PROMPT, input, atomExtractionSchema());
        return parseExtractedAtoms(content);
    }
    async judgeAtomUtility(input) {
        const content = await this.chatJson(ATOM_UTILITY_SYSTEM_PROMPT, input, atomUtilitySchema());
        return parseAtomUtilityVerdict(content);
    }
    async chatJson(systemPrompt, input, schema) {
        let response;
        try {
            response = await this.fetchFn(`${this.ollamaUrl}/api/chat`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    model: this.modelId,
                    messages: [
                        { role: 'system', content: systemPrompt },
                        { role: 'user', content: JSON.stringify(input) },
                    ],
                    format: schema,
                    stream: true,
                }),
                signal: AbortSignal.timeout(this.timeoutMs),
            });
        }
        catch (error) {
            throw new ModelProviderError('Ollama generation request failed.', error);
        }
        if (!response.ok) {
            const detail = await response.text();
            throw new ModelProviderError(`Ollama generation request failed: ${response.status} ${detail}`);
        }
        const content = await this.readStreamedContent(response);
        if (!content.trim()) {
            throw new ModelProviderError('Ollama generation response did not include message content.');
        }
        return content;
    }
    /**
     * Read an NDJSON `/api/chat` stream, accumulating `message.content` across
     * chunks until `done === true`. Each line is a JSON object; an `error`
     * string aborts the stream. Read/parse failures are wrapped in
     * ModelProviderError so no raw SyntaxError/TypeError escapes.
     *
     * The reader is always cancelled in `finally`: on any throw path (invalid
     * JSON, mid-stream error line) an undrained body would otherwise hold the
     * connection open against a real server still streaming a long generation.
     * `cancel()` is a no-op after natural exhaustion or the `done:true` early
     * return, so behavior is otherwise identical.
     */
    async readStreamedContent(response) {
        const body = response.body;
        if (!body) {
            throw new ModelProviderError('Ollama generation response had no readable body.');
        }
        const reader = body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let content = '';
        const handleLine = (line) => {
            const trimmed = line.trim();
            if (!trimmed)
                return false;
            let parsed;
            try {
                parsed = JSON.parse(trimmed);
            }
            catch (error) {
                throw new ModelProviderError('Ollama generation stream emitted invalid JSON.', error);
            }
            if (typeof parsed.error === 'string') {
                throw new ModelProviderError(`Ollama generation stream reported an error: ${parsed.error}`);
            }
            content += parsed.message?.content ?? '';
            return parsed.done === true;
        };
        try {
            for (;;) {
                const { value, done } = await reader.read();
                if (done)
                    break;
                buffer += decoder.decode(value, { stream: true });
                let newlineIndex;
                while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
                    const line = buffer.slice(0, newlineIndex);
                    buffer = buffer.slice(newlineIndex + 1);
                    if (handleLine(line))
                        return content;
                }
            }
            // Flush any trailing partial line that was not newline-terminated.
            buffer += decoder.decode();
            if (buffer.trim()) {
                handleLine(buffer);
            }
            return content;
        }
        catch (error) {
            if (error instanceof ModelProviderError)
                throw error;
            throw new ModelProviderError('Ollama generation stream read failed.', error);
        }
        finally {
            reader.cancel().catch(() => { });
        }
    }
}
function trimTrailingSlash(url) {
    return url.endsWith('/') ? url.slice(0, -1) : url;
}
