import { DeepSeekCodeAbortError, isAbortError, throwIfAborted } from "../../utils/abort.js";
import { baseTools } from "../../tools/registry.js";
import { captureProviderPrompt } from "../telemetry/providerPromptAudit.js";
import { envelopeActionsToToolCalls, feedbackToUserMessage, reportToToolMessages, toNativeFunctionTools, toolCallsToActions, } from "./toolCalling.js";
export class DeepSeekClient {
    config;
    lastUsage;
    lastReasoning;
    constructor(config) {
        this.config = config;
    }
    get providerName() {
        return this.config.name;
    }
    get model() {
        return this.config.model;
    }
    async verifyModel() {
        return this.completeChat([
            { role: "system", content: "You are a terse health-check responder." },
            { role: "user", content: "Reply with exactly: ok" },
        ]);
    }
    async completeChat(messages, options = {}) {
        const json = await this.requestJson({
            model: this.config.model,
            messages,
            temperature: 0.3,
            max_tokens: Math.min(this.config.maxOutputTokens ?? 1200, 1200),
        }, options.signal, "chat");
        const message = json.choices?.[0]?.message;
        const usage = usageFromPayload(json.usage);
        this.lastUsage = usage;
        this.lastReasoning = message?.reasoning_content ?? undefined;
        return {
            provider: this.config.name,
            model: json.model ?? this.config.model,
            text: message?.content ?? "",
            reasoning: message?.reasoning_content ?? undefined,
            ...usage,
        };
    }
    takeLastUsage() {
        const usage = this.lastUsage;
        this.lastUsage = undefined;
        return usage;
    }
    takeLastReasoning() {
        const reasoning = this.lastReasoning;
        this.lastReasoning = undefined;
        return reasoning;
    }
    async *streamChat(messages, options = {}) {
        const response = await this.requestRaw({
            model: this.config.model,
            messages,
            temperature: 0.5,
            stream: true,
            stream_options: { include_usage: true },
            max_tokens: this.config.maxOutputTokens ?? 1200,
        }, options.signal, "chat_stream");
        if (!response.body)
            throw new Error("DeepSeek stream response has no body");
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        const dataLines = [];
        const flushEvent = function* () {
            const event = dataLines.join("\n").trim();
            dataLines.length = 0;
            if (!event || event === "[DONE]")
                return;
            let chunk;
            try {
                chunk = JSON.parse(event);
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                throw new Error(`Invalid DeepSeek stream event: ${message}; data=${compact(event, 240)}`);
            }
            const delta = chunk.choices?.[0]?.delta;
            if (delta?.reasoning_content) {
                yield { type: "reasoning_delta", text: delta.reasoning_content };
            }
            if (delta?.content) {
                yield { type: "text_delta", text: delta.content };
            }
            if (chunk.usage) {
                yield { type: "usage", ...usageFromPayload(chunk.usage) };
            }
        };
        const readLine = function* (line) {
            const trimmed = line.trimEnd();
            if (!trimmed) {
                yield* flushEvent();
                return;
            }
            if (trimmed.startsWith(":"))
                return;
            if (!trimmed.startsWith("data:"))
                return;
            dataLines.push(trimmed.slice("data:".length).trimStart());
        };
        while (true) {
            throwIfAborted(options.signal);
            const { value, done } = await reader.read();
            if (done)
                break;
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split(/\r?\n/);
            buffer = lines.pop() ?? "";
            for (const line of lines) {
                yield* readLine(line);
            }
        }
        buffer += decoder.decode();
        if (buffer) {
            yield* readLine(buffer);
        }
        yield* flushEvent();
    }
    async classifyTurn(input, options = {}) {
        const reply = await this.completeJson([
            {
                role: "system",
                content: "Return json only. Classify the user request for a local coding agent. " +
                    "Do not decide by keywords alone. If local files, commands, browser, docs, or multi-agent work are needed, set needs_local_tools true.",
            },
            {
                role: "user",
                content: `User request:\n${input}\n\n` +
                    'Return json like {"task_kind":"chat|clarification|file_change|command|browser|document|research|multi_agent|computer_use|other","needs_local_tools":false,"reason":"short reason"}.',
            },
        ], {
            label: "turn classification",
            maxTokens: Math.min(this.config.maxOutputTokens ?? 1200, 900),
            signal: options.signal,
        });
        return normalizeClassification(reply);
    }
    async planActions(input, options = {}) {
        const messages = buildNativeToolPlanningMessages(input);
        const planningTools = toolsForRequest(input.availableToolNames);
        const json = await this.requestJson({
            model: this.config.model,
            messages,
            tools: toNativeFunctionTools(planningTools),
            tool_choice: "auto",
            temperature: 0.2,
            max_tokens: Math.max(this.config.maxOutputTokens ?? 1200, 2200),
        }, options.signal, "native tool plan");
        const usage = usageFromPayload(json.usage);
        this.lastUsage = usage;
        const message = json.choices?.[0]?.message;
        this.lastReasoning = message?.reasoning_content ?? undefined;
        const toolCalls = message?.tool_calls ?? [];
        if (toolCalls.length > 0) {
            let actions;
            try {
                actions = toolCallsToActions(toolCalls);
            }
            catch (error) {
                const detail = error instanceof Error ? error.message : String(error);
                throw new Error(`DeepSeek native tool call failed local schema validation: ${detail}`);
            }
            return {
                task_kind: "tool_calling",
                needs_local_tools: true,
                acceptance_criteria: [],
                final_message: message?.content?.trim() ?? "",
                continue_work: true,
                remaining_work: "Continue from native tool results.",
                actions,
            };
        }
        const finalText = message?.content?.trim() ?? "";
        if (isInitialLocalToolPlan(input)) {
            throw new Error([
                "DeepSeek native tool calling did not return any tool_calls for a local-tool request.",
                "The selected model or gateway may not support tool calling, or it ignored the tools schema.",
                "DeepSeekCode will not fall back to the old ActionEnvelope JSON planner; switch to a tool-calling model/provider and retry.",
                finalText ? `model_content=${compact(finalText, 300)}` : "",
            ].filter(Boolean).join(" "));
        }
        return {
            task_kind: "chat",
            needs_local_tools: false,
            acceptance_criteria: [],
            final_message: finalText,
            continue_work: false,
            actions: [],
        };
    }
    async completeJson(messages, options = {}) {
        const maxTokens = options.maxTokens ?? this.config.maxOutputTokens ?? 1200;
        const json = await this.requestJson({
            model: this.config.model,
            messages,
            temperature: 0.2,
            thinking: { type: "disabled" },
            response_format: { type: "json_object" },
            max_tokens: maxTokens,
        }, options.signal, options.label ?? "json response");
        this.lastUsage = usageFromPayload(json.usage);
        this.lastReasoning = json.choices?.[0]?.message?.reasoning_content ?? undefined;
        const first = parseJsonResponse(json, options.label ?? "json response");
        if (first.ok)
            return first.value;
        throw new Error(`DeepSeek JSON response invalid: ${first.error}`);
    }
    async requestJson(body, signal, auditLabel = "json") {
        const response = await this.requestRaw(body, signal, auditLabel);
        try {
            return (await response.json());
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            throw new Error(`DeepSeek API response was not valid JSON during ${auditLabel}: ${message}`);
        }
    }
    async requestRaw(body, signal, auditLabel = "request") {
        captureProviderPrompt({
            provider: this.config.name,
            model: this.config.model,
            label: auditLabel,
            body,
        });
        const maxAttempts = isStreamingRequest(body) ? 1 : 3;
        let lastError;
        for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(new Error("DeepSeek request timed out")), this.config.timeoutSecs * 1000);
            const abortFromCaller = () => controller.abort(new DeepSeekCodeAbortError(signal?.reason));
            if (signal?.aborted)
                abortFromCaller();
            else
                signal?.addEventListener("abort", abortFromCaller, { once: true });
            try {
                const response = await fetch(endpointFor(this.config.baseUrl), {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                        Authorization: `Bearer ${this.config.apiKey}`,
                    },
                    body: JSON.stringify(body),
                    signal: controller.signal,
                });
                if (response.ok)
                    return response;
                const text = await response.text();
                if (attempt < maxAttempts && isRetryableStatus(response.status)) {
                    await sleep(retryDelayMs(attempt), signal);
                    continue;
                }
                throw new Error(formatApiError(response.status, text, body));
            }
            catch (error) {
                if (isAbortError(error, signal))
                    throw error;
                lastError = error;
                if (attempt >= maxAttempts || !isRetryableNetworkError(error))
                    throw error;
                await sleep(retryDelayMs(attempt), signal);
            }
            finally {
                clearTimeout(timeout);
                signal?.removeEventListener("abort", abortFromCaller);
            }
        }
        throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "DeepSeek request failed"));
    }
}
function toolsForRequest(availableToolNames) {
    if (!availableToolNames || availableToolNames.length === 0)
        return baseTools;
    const allowed = new Set(availableToolNames);
    return baseTools.filter((tool) => allowed.has(tool.name));
}
function isInitialLocalToolPlan(input) {
    return !input.feedback && (!input.trajectory || input.trajectory.length === 0);
}
export function buildNativeToolPlanningMessages(input) {
    const messages = [
        {
            role: "system",
            content: [
                input.systemPrompt,
                "",
                "Native tool calling mode is active.",
                "Use function tool calls for local work, exactly like ClaudeCode emits tool_use blocks.",
                "After tool results, continue with the next useful tool call or provide the final answer.",
                "Use at most 3 tool calls in one assistant turn; split large work into additional turns.",
                "For large generated files, use a compact write_file first section followed by append_file chunks. Do not put a whole large artifact into one JSON tool argument.",
                "When chunking files, preserve the target language or document syntax in the final assembled file.",
                "For ordinary chat or a completed task, answer normally without tool calls.",
            ].join("\n"),
        },
        {
            role: "user",
            content: [
                input.contextSummary ? `Project context:\n${input.contextSummary}` : "",
                `Current user request:\n${input.userMessage}`,
            ].filter(Boolean).join("\n\n"),
        },
    ];
    const turns = (input.trajectory ?? []).slice(-6);
    for (const turn of turns) {
        const toolCalls = envelopeActionsToToolCalls(turn);
        if (toolCalls.length === 0) {
            messages.push({
                role: "assistant",
                content: turn.assistantEnvelope.final_message || "(no tool call)",
            });
            continue;
        }
        messages.push({
            role: "assistant",
            content: turn.assistantEnvelope.final_message || null,
            reasoning_content: turn.reasoning || fallbackToolReasoning(turn),
            tool_calls: toolCalls,
        });
        messages.push(...reportToToolMessages(turn));
    }
    const feedbackAlreadyInTrajectory = turns.some((turn) => turn.toolReport === input.feedback);
    if (input.feedback && !feedbackAlreadyInTrajectory) {
        messages.push({
            role: "user",
            content: feedbackToUserMessage(input.feedback),
        });
    }
    return messages;
}
function parseJsonResponse(json, label) {
    const choice = json.choices?.[0];
    const content = stripJsonFence(choice?.message?.content ?? "");
    const reasoningChars = choice?.message?.reasoning_content?.length ?? 0;
    const finish = choice?.finish_reason ?? "unknown";
    return parseJsonText(content, { label, finish, reasoningChars });
}
function parseJsonText(text, meta) {
    const content = stripJsonFence(text);
    if (!content.trim()) {
        return {
            ok: false,
            error: `${meta.label} was empty finish=${meta.finish ?? "unknown"} reasoningChars=${meta.reasoningChars}`,
        };
    }
    try {
        return { ok: true, value: JSON.parse(content) };
    }
    catch (error) {
        const balanced = extractFirstJsonObject(content);
        if (balanced && balanced !== content) {
            try {
                return { ok: true, value: JSON.parse(balanced) };
            }
            catch {
                // Preserve the original parse error below; the extracted object was
                // only a best-effort recovery path for trailing prose or duplicate JSON.
            }
        }
        const message = error instanceof Error ? error.message : String(error);
        return {
            ok: false,
            error: `${meta.label} parse failed: ${message} finish=${meta.finish ?? "unknown"} contentChars=${content.length} head=${JSON.stringify(compact(content, 160))}`,
        };
    }
}
function extractFirstJsonObject(text) {
    const start = text.indexOf("{");
    if (start < 0)
        return undefined;
    let depth = 0;
    let inString = false;
    let escaping = false;
    for (let index = start; index < text.length; index += 1) {
        const char = text[index];
        if (inString) {
            if (escaping) {
                escaping = false;
            }
            else if (char === "\\") {
                escaping = true;
            }
            else if (char === "\"") {
                inString = false;
            }
            continue;
        }
        if (char === "\"") {
            inString = true;
            continue;
        }
        if (char === "{")
            depth += 1;
        if (char === "}") {
            depth -= 1;
            if (depth === 0)
                return text.slice(start, index + 1);
        }
    }
    return undefined;
}
function endpointFor(baseUrl) {
    return `${baseUrl.replace(/\/+$/, "")}/chat/completions`;
}
function isStreamingRequest(body) {
    return body.stream === true;
}
function isRetryableStatus(status) {
    return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
}
function retryDelayMs(attempt) {
    return Math.min(2_000, 250 * 2 ** Math.max(0, attempt - 1));
}
function isRetryableNetworkError(error) {
    if (error instanceof DeepSeekCodeAbortError)
        return false;
    const message = error instanceof Error ? error.message : String(error);
    return /fetch failed|network|socket|econnreset|econnrefused|etimedout|timeout|terminated/i.test(message);
}
async function sleep(ms, signal) {
    if (ms <= 0)
        return;
    if (signal?.aborted)
        throw new DeepSeekCodeAbortError(signal.reason);
    await new Promise((resolve, reject) => {
        const done = () => {
            signal?.removeEventListener("abort", onAbort);
            resolve();
        };
        const timer = setTimeout(done, ms);
        const onAbort = () => {
            clearTimeout(timer);
            reject(new DeepSeekCodeAbortError(signal?.reason));
        };
        signal?.addEventListener("abort", onAbort, { once: true });
    });
}
function formatApiError(status, text, body) {
    const hasTools = Array.isArray(body.tools);
    const label = hasTools ? "native tool calling request" : "request";
    const detail = compact(text || "(empty response body)", 1_000);
    const hint = apiErrorHint(status, hasTools, text);
    return [`DeepSeek API ${status} failed during ${label}: ${detail}`, hint].filter(Boolean).join(" ");
}
function apiErrorHint(status, hasTools, text = "") {
    if (status === 400 && /reasoning_content/i.test(text)) {
        return "DeepSeek thinking mode requires assistant reasoning_content to be replayed with prior tool calls.";
    }
    if (status === 400 && hasTools) {
        return "The selected model or gateway rejected the tools schema; choose a tool-calling model or reduce/repair the schema.";
    }
    if (status === 401 || status === 403) {
        return "Check the configured API key and provider permissions.";
    }
    if (status === 404 && hasTools) {
        return "The selected provider endpoint may not support chat/completions tool calling.";
    }
    if (status === 429) {
        return "Rate limited; DeepSeekCode retried network-safe attempts and then stopped.";
    }
    if (status >= 500) {
        return "Provider-side error; retry later or switch model/provider.";
    }
    return "";
}
function fallbackToolReasoning(turn) {
    return turn.note
        ? `DeepSeekCode replayed a compact local tool turn: ${compact(turn.note, 240)}`
        : "DeepSeekCode replayed a compact local tool turn for native tool-result continuity.";
}
function usageFromPayload(payload) {
    if (!payload)
        return {};
    return {
        inputTokens: payload.prompt_tokens,
        outputTokens: payload.completion_tokens,
        cacheHitTokens: payload.prompt_cache_hit_tokens ?? payload.prompt_tokens_details?.cached_tokens,
        cacheMissTokens: payload.prompt_cache_miss_tokens,
    };
}
function normalizeClassification(value) {
    const candidate = value && typeof value === "object" ? value : {};
    return {
        task_kind: typeof candidate.task_kind === "string" ? candidate.task_kind : "chat",
        needs_local_tools: Boolean(candidate.needs_local_tools),
        reason: typeof candidate.reason === "string" ? candidate.reason : "model classification",
    };
}
function stripJsonFence(text) {
    const trimmed = text.trim();
    const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
    return match ? match[1] : trimmed;
}
function compact(text, max) {
    return text.replace(/\s+/g, " ").trim().slice(0, max);
}
