/**
 * Anthropic adapter for the LLMClient port.
 *
 * Uses fetch() against the Messages API — same transport pattern as the
 * existing grader. No SDK dependency. Centralizes retry, rate-limit handling,
 * cost accounting, and per-call telemetry tagging via `context.feature`.
 *
 * Anthropic does not have a first-class JSON mode like OpenAI. For
 * `completeStructured`, the adapter uses the API's top-level `system`
 * field to instruct the model to return JSON only (top-level system is
 * harder for user-controlled content in `prompt` to override than a
 * user-turn prefix), then strips any surrounding ``` fences before
 * parsing through the Zod schema (parse-don't-validate).
 *
 * Per `.claude/rules/eval-pipeline.md` and `.claude/rules/typescript.md`:
 * the adapter never reads `process.env`. Typed constructor args only.
 */
import { AnthropicResponseSchema, splitModelId, } from "../../_vendor/ailf-core/index.js";
import { DEFAULT_RETRY_POLICY, parseRetryAfterSeconds, runWithRetry, } from "./retry.js";
const DEFAULT_BASE_URL = "https://api.anthropic.com/v1/messages";
const DEFAULT_API_VERSION = "2023-06-01";
const DEFAULT_MAX_TOKENS = 4096;
/**
 * Pricing reference: https://www.anthropic.com/pricing#api
 * Update when models or vendor pricing changes.
 */
const DEFAULT_PRICING = {
    "claude-opus-4-6": { inputPer1k: 0.015, outputPer1k: 0.075 },
    "claude-opus-4-5-20251101": { inputPer1k: 0.015, outputPer1k: 0.075 },
    "claude-sonnet-4-6": { inputPer1k: 0.003, outputPer1k: 0.015 },
};
const STRUCTURED_SYSTEM = "Respond with only a single JSON object that conforms to the requested schema. " +
    "Do not include any prose, commentary, or markdown code fences. " +
    "Return raw JSON only.";
export class AnthropicLLMClient {
    apiKey;
    baseUrl;
    apiVersion;
    pricing;
    retryPolicy;
    logger;
    sleep;
    rng;
    constructor(options) {
        this.apiKey = options.apiKey;
        this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
        this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
        this.pricing = { ...DEFAULT_PRICING, ...(options.pricing ?? {}) };
        this.retryPolicy = {
            ...DEFAULT_RETRY_POLICY,
            ...(options.retryPolicy ?? {}),
        };
        if (options.logger)
            this.logger = options.logger;
        if (options.sleep)
            this.sleep = options.sleep;
        if (options.rng)
            this.rng = options.rng;
    }
    async complete(args) {
        const { modelName } = splitModelId(args.model);
        const body = buildBody(modelName, args.prompt, {
            temperature: args.temperature,
            maxTokens: args.maxTokens,
            stop: args.stop,
        });
        const data = await this.callApi(body);
        const text = extractText(data.content);
        if (text === "") {
            throw new Error(`Anthropic returned empty completion for model ${args.model}`);
        }
        const usage = extractUsage(data.usage);
        const cost = this.computeCost(modelName, usage);
        this.logTelemetry(args.context, args.model, usage, cost);
        return { text, usage, cost, model: args.model };
    }
    async completeStructured(args) {
        const { modelName } = splitModelId(args.model);
        const body = buildBody(modelName, args.prompt, {
            temperature: args.temperature,
            maxTokens: args.maxTokens,
            system: STRUCTURED_SYSTEM,
        });
        const data = await this.callApi(body);
        const raw = extractText(data.content);
        if (raw === "") {
            throw new Error(`Anthropic returned empty structured completion for model ${args.model}`);
        }
        const stripped = stripJsonFence(raw);
        let parsed;
        try {
            parsed = JSON.parse(stripped);
        }
        catch (err) {
            throw new Error(`Anthropic structured completion returned invalid JSON for model ${args.model}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
        }
        const value = args.schema.parse(parsed);
        const usage = extractUsage(data.usage);
        const cost = this.computeCost(modelName, usage);
        this.logTelemetry(args.context, args.model, usage, cost);
        return { value, usage, cost, model: args.model };
    }
    async callApi(body) {
        return runWithRetry({
            policy: this.retryPolicy,
            ...(this.sleep ? { sleep: this.sleep } : {}),
            ...(this.rng ? { rng: this.rng } : {}),
            attempt: async () => {
                const response = await fetch(this.baseUrl, {
                    method: "POST",
                    headers: {
                        "x-api-key": this.apiKey,
                        "anthropic-version": this.apiVersion,
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify(body),
                });
                if (!response.ok) {
                    const text = await response.text();
                    const retryAfter = parseRetryAfterSeconds(response.headers.get("retry-after"));
                    return {
                        ok: false,
                        status: response.status,
                        body: text,
                        ...(retryAfter !== undefined
                            ? { retryAfterSeconds: retryAfter }
                            : {}),
                    };
                }
                const json = await response.json();
                const data = AnthropicResponseSchema.parse(json);
                if (data.error?.message) {
                    throw new Error(`Anthropic API error: ${data.error.message}`);
                }
                return { ok: true, value: data };
            },
        });
    }
    computeCost(modelName, usage) {
        const price = this.pricing[modelName];
        if (!price) {
            this.logger?.warn(`Anthropic cost unknown for model "${modelName}" — recording cost=0. Add it to AnthropicLLMClientOptions.pricing.`);
            return 0;
        }
        return ((usage.promptTokens / 1000) * price.inputPer1k +
            (usage.completionTokens / 1000) * price.outputPer1k);
    }
    logTelemetry(context, model, usage, cost) {
        if (!this.logger)
            return;
        const tag = context ? ` feature=${context.feature}` : "";
        const runTag = context?.runId ? ` runId=${context.runId}` : "";
        const cardTag = context?.cardId ? ` cardId=${context.cardId}` : "";
        this.logger.debug(`LLM call (anthropic)${tag}${runTag}${cardTag} model=${model} ` +
            `prompt_tokens=${usage.promptTokens} completion_tokens=${usage.completionTokens} ` +
            `cost_usd=${cost.toFixed(6)}`);
    }
}
function buildBody(modelName, prompt, opts) {
    const body = {
        model: modelName,
        max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
        messages: [{ role: "user", content: prompt }],
    };
    if (opts.temperature !== undefined)
        body.temperature = opts.temperature;
    if (opts.stop && opts.stop.length > 0)
        body.stop_sequences = opts.stop;
    if (opts.system)
        body.system = opts.system;
    return body;
}
/**
 * Concatenate every `text` content block. Anthropic responses can interleave
 * `text` and `tool_use` blocks; for non-tool calls there's typically one
 * text block, but joining is the robust default.
 */
function extractText(content) {
    if (!content)
        return "";
    const parts = [];
    for (const block of content) {
        if (block.type === "text" && typeof block.text === "string") {
            parts.push(block.text);
        }
    }
    return parts.join("");
}
function extractUsage(usage) {
    return {
        promptTokens: usage?.input_tokens ?? 0,
        completionTokens: usage?.output_tokens ?? 0,
    };
}
/**
 * Strip a single ```json ... ``` or ``` ... ``` fence wrapper if present.
 * Anthropic occasionally wraps JSON despite the system instruction.
 */
function stripJsonFence(text) {
    const trimmed = text.trim();
    const fenceMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
    if (fenceMatch)
        return fenceMatch[1].trim();
    return trimmed;
}
