/**
 * TraceCollector — extracts structured trace data from provider responses.
 *
 * Parses tool calls, token usage, and timing data from Promptfoo result
 * objects and normalizes them into the canonical `EvalTrace` shape.
 *
 * Works via inline extraction — parsing provider response metadata
 * directly, without requiring additional infrastructure.
 *
 * @see docs/design-docs/architecture-overhaul/observability-telemetry.md
 * @see packages/core/src/types/trace.ts — EvalTrace types
 */
import { classifyToolCall } from "./tool-classifier.js";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Collect a trace from a single provider response.
 *
 * Extracts tool calls, token usage, timing, and builds the
 * chronological event log.
 */
export function collectTrace(response, options) {
    const now = new Date().toISOString();
    const traceId = `trace-${options.runId}-${options.taskId}-${options.testCaseIndex}`;
    // Extract tool calls
    const toolCalls = extractToolCalls(response, options.toolCategories, options.maxOutputBytes ?? 10_240);
    // Extract token usage
    const tokensUsed = extractTokenUsage(response);
    // Compute cost (caller provides pricing separately)
    const durationMs = response.latencyMs ?? 0;
    // Build event log
    const events = buildEventLog(toolCalls, tokensUsed, durationMs, now);
    // Build spans — one root span for the test case
    const spans = buildSpans(options, durationMs);
    // Extract URLs and search terms from tool calls
    const { urlsVisited, searchTerms, filesRead, filesWritten } = extractAccessPatterns(toolCalls);
    return {
        traceId,
        runId: options.runId,
        taskId: options.taskId,
        testCaseIndex: options.testCaseIndex,
        modelId: options.modelId,
        spans,
        toolCalls,
        urlsVisited,
        searchTerms,
        filesRead,
        filesWritten,
        tokensUsed,
        costEstimate: 0, // Set by cost calculator separately
        durationMs,
        events,
        startedAt: now,
        completedAt: now,
    };
}
/**
 * Merge multiple per-turn traces into a single test case trace.
 *
 * Each turn produces its own trace. This function combines them into
 * a parent trace with per-turn spans.
 */
export function mergeTraces(turns, parentOptions) {
    if (turns.length === 0) {
        return collectTrace({}, parentOptions);
    }
    const first = turns[0];
    const last = turns[turns.length - 1];
    const traceId = `trace-${parentOptions.runId}-${parentOptions.taskId}-${parentOptions.testCaseIndex}`;
    // Merge all tool calls, events, and access patterns
    const toolCalls = turns.flatMap((t) => t.toolCalls);
    const events = turns.flatMap((t) => t.events);
    const urlsVisited = [...new Set(turns.flatMap((t) => t.urlsVisited))];
    const searchTerms = [...new Set(turns.flatMap((t) => t.searchTerms))];
    const filesRead = [...new Set(turns.flatMap((t) => t.filesRead))];
    const filesWritten = [...new Set(turns.flatMap((t) => t.filesWritten))];
    // Aggregate token usage
    const tokensUsed = {
        promptTokens: turns.reduce((sum, t) => sum + t.tokensUsed.promptTokens, 0),
        completionTokens: turns.reduce((sum, t) => sum + t.tokensUsed.completionTokens, 0),
        totalTokens: turns.reduce((sum, t) => sum + t.tokensUsed.totalTokens, 0),
        toolTokens: turns.reduce((sum, t) => sum + (t.tokensUsed.toolTokens ?? 0), 0),
    };
    // Build per-turn spans
    const spans = [
        {
            spanId: `${traceId}-root`,
            parentSpanId: null,
            operation: "test-case",
            startMs: 0,
            endMs: turns.reduce((sum, t) => sum + t.durationMs, 0),
            attributes: { turnCount: turns.length },
        },
        ...turns.map((turn, i) => ({
            spanId: `${traceId}-turn-${i}`,
            parentSpanId: `${traceId}-root`,
            operation: `turn-${i}`,
            startMs: turns.slice(0, i).reduce((sum, t) => sum + t.durationMs, 0),
            endMs: turns.slice(0, i + 1).reduce((sum, t) => sum + t.durationMs, 0),
            attributes: {
                modelId: turn.modelId,
                toolCallCount: turn.toolCalls.length,
            },
        })),
    ];
    return {
        traceId,
        runId: parentOptions.runId,
        taskId: parentOptions.taskId,
        testCaseIndex: parentOptions.testCaseIndex,
        modelId: parentOptions.modelId,
        spans,
        toolCalls,
        urlsVisited,
        searchTerms,
        filesRead,
        filesWritten,
        tokensUsed,
        costEstimate: turns.reduce((sum, t) => sum + t.costEstimate, 0),
        durationMs: turns.reduce((sum, t) => sum + t.durationMs, 0),
        events,
        startedAt: first.startedAt,
        completedAt: last.completedAt,
    };
}
// ---------------------------------------------------------------------------
// Tool call extraction
// ---------------------------------------------------------------------------
function extractToolCalls(response, customCategories, maxOutputBytes) {
    const rawCalls = response.metadata?.toolCalls ?? [];
    return rawCalls.map((raw) => {
        const name = raw.name ?? raw.function?.name ?? "unknown";
        const input = raw.input ?? parseJsonSafe(raw.function?.arguments) ?? {};
        let output = raw.output;
        // Truncate output if too large
        if (maxOutputBytes && output) {
            const serialized = JSON.stringify(output);
            if (serialized.length > maxOutputBytes) {
                output = serialized.slice(0, maxOutputBytes) + "... [truncated]";
            }
        }
        return {
            name,
            input: input,
            output,
            durationMs: raw.durationMs ?? 0,
            error: raw.error,
            category: classifyToolCall(name, customCategories),
        };
    });
}
// ---------------------------------------------------------------------------
// Token usage extraction
// ---------------------------------------------------------------------------
function extractTokenUsage(response) {
    const usage = response.tokenUsage;
    if (!usage) {
        return {
            promptTokens: 0,
            completionTokens: 0,
            totalTokens: 0,
        };
    }
    const prompt = usage.prompt ?? 0;
    const completion = usage.completion ?? 0;
    return {
        promptTokens: prompt,
        completionTokens: completion,
        totalTokens: usage.total ?? prompt + completion,
        ...(usage.cached ? { toolTokens: usage.cached } : {}),
    };
}
// ---------------------------------------------------------------------------
// Access pattern extraction
// ---------------------------------------------------------------------------
function extractAccessPatterns(toolCalls) {
    const urlsVisited = new Set();
    const searchTerms = new Set();
    const filesRead = new Set();
    const filesWritten = new Set();
    for (const call of toolCalls) {
        switch (call.category) {
            case "navigate":
            case "read": {
                const url = extractString(call.input, "url");
                if (url)
                    urlsVisited.add(url);
                const filePath = extractString(call.input, "path", "file", "filename");
                if (filePath) {
                    if (call.category === "read")
                        filesRead.add(filePath);
                }
                break;
            }
            case "search": {
                const query = extractString(call.input, "query", "term", "search");
                if (query)
                    searchTerms.add(query);
                break;
            }
            case "write": {
                const writePath = extractString(call.input, "path", "file", "filename");
                if (writePath)
                    filesWritten.add(writePath);
                break;
            }
        }
    }
    return {
        urlsVisited: [...urlsVisited],
        searchTerms: [...searchTerms],
        filesRead: [...filesRead],
        filesWritten: [...filesWritten],
    };
}
// ---------------------------------------------------------------------------
// Event log
// ---------------------------------------------------------------------------
function buildEventLog(toolCalls, tokensUsed, durationMs, timestamp) {
    const events = [];
    // LLM request event
    events.push({
        timestamp,
        type: "llm_request",
        data: { promptTokens: tokensUsed.promptTokens },
    });
    // Tool call events
    for (const call of toolCalls) {
        events.push({
            timestamp,
            type: "tool_call_start",
            data: { name: call.name, category: call.category },
        });
        events.push({
            timestamp,
            type: "tool_call_end",
            data: {
                name: call.name,
                durationMs: call.durationMs,
                ...(call.error ? { error: call.error } : {}),
            },
        });
    }
    // LLM response event
    events.push({
        timestamp,
        type: "llm_response",
        data: {
            completionTokens: tokensUsed.completionTokens,
            totalTokens: tokensUsed.totalTokens,
            durationMs,
        },
    });
    return events;
}
// ---------------------------------------------------------------------------
// Span building
// ---------------------------------------------------------------------------
function buildSpans(options, durationMs) {
    const traceId = `trace-${options.runId}-${options.taskId}-${options.testCaseIndex}`;
    return [
        {
            spanId: `${traceId}-root`,
            parentSpanId: null,
            operation: "test-case",
            startMs: 0,
            endMs: durationMs,
            attributes: {
                taskId: options.taskId,
                modelId: options.modelId,
                testCaseIndex: options.testCaseIndex,
            },
        },
    ];
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function parseJsonSafe(str) {
    if (!str)
        return null;
    try {
        return JSON.parse(str);
    }
    catch {
        return null;
    }
}
function extractString(input, ...keys) {
    for (const key of keys) {
        const val = input[key];
        if (typeof val === "string" && val.length > 0)
            return val;
    }
    return undefined;
}
