import { GlobalStore } from "../state/globalStore.js";
// --- Helpers ---
export function makeBaseEvent(checkpoint, step) {
    return {
        step,
        ...checkpoint.location,
    };
}
function isInternalVar(name) {
    return name.startsWith("__");
}
function valuesEqual(a, b) {
    if (a === b)
        return true;
    if (a == null || b == null)
        return false;
    if (typeof a !== typeof b)
        return false;
    if (typeof a !== "object")
        return false;
    return JSON.stringify(a) === JSON.stringify(b);
}
function diffObject(prev, curr) {
    const changes = [];
    for (const key of Object.keys(curr)) {
        if (isInternalVar(key))
            continue;
        const prevVal = prev[key];
        const currVal = curr[key];
        if (!valuesEqual(prevVal, currVal)) {
            changes.push({ key, value: currVal, previousValue: prevVal ?? null });
        }
    }
    return changes;
}
function getMessages(checkpoint) {
    const frame = checkpoint.stack.stack.at(-1);
    if (!frame?.threads)
        return [];
    const { threads, activeStack } = frame.threads;
    const threadIds = Object.keys(threads);
    if (threadIds.length === 0)
        return [];
    const activeId = activeStack.findLast((id) => threads[id] != null) ?? threadIds[0];
    return threads[activeId]?.messages ?? [];
}
function getMessageContent(message) {
    if (typeof message.content === "string")
        return message.content;
    if (Array.isArray(message.content)) {
        return message.content.map((part) => part.text ?? "").join("");
    }
    if (message.content == null && message.toolCalls) {
        return message.toolCalls
            .map((tc) => `Tool call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
            .join("\n");
    }
    return message.content == null ? "" : JSON.stringify(message.content);
}
function getTokenUsageDiff(prev, curr) {
    const prevStats = prev.globals.store?.[GlobalStore.INTERNAL_MODULE]?.["__tokenStats"];
    const currStats = curr.globals.store?.[GlobalStore.INTERNAL_MODULE]?.["__tokenStats"];
    if (!currStats?.usage)
        return null;
    if (!prevStats?.usage)
        return { ...currStats.usage };
    return {
        inputTokens: (currStats.usage.inputTokens ?? 0) -
            (prevStats.usage.inputTokens ?? 0),
        outputTokens: (currStats.usage.outputTokens ?? 0) -
            (prevStats.usage.outputTokens ?? 0),
        cachedInputTokens: (currStats.usage.cachedInputTokens ?? 0) -
            (prevStats.usage.cachedInputTokens ?? 0),
        totalTokens: (currStats.usage.totalTokens ?? 0) -
            (prevStats.usage.totalTokens ?? 0),
    };
}
// --- Detectors ---
export function detectNodeTransitions(prev, curr, step) {
    const events = [];
    if (prev === null) {
        events.push({
            ...makeBaseEvent(curr, step),
            type: "node-enter",
            nodeName: curr.nodeId,
        });
        return events;
    }
    if (prev.nodeId !== curr.nodeId) {
        events.push({
            ...makeBaseEvent(prev, step),
            type: "node-exit",
            nodeName: prev.nodeId,
        });
        events.push({
            ...makeBaseEvent(curr, step),
            type: "node-enter",
            nodeName: curr.nodeId,
        });
    }
    return events;
}
export function detectStackChanges(prev, curr, step) {
    if (!prev)
        return [];
    const events = [];
    const prevDepth = prev.stack.stack.length;
    const currDepth = curr.stack.stack.length;
    if (currDepth > prevDepth) {
        for (let i = prevDepth; i < currDepth; i++) {
            const frame = curr.stack.stack[i];
            events.push({
                ...makeBaseEvent(curr, step),
                type: "function-enter",
                functionName: curr.scopeName,
                args: { ...frame.args },
            });
        }
    }
    else if (currDepth < prevDepth) {
        const callerFrame = curr.stack.stack.at(-1);
        const prevCallerFrame = prev.stack.stack[currDepth - 1];
        let returnValue = undefined;
        if (callerFrame && prevCallerFrame) {
            for (const key of Object.keys(callerFrame.locals)) {
                if (!isInternalVar(key) && !(key in prevCallerFrame.locals)) {
                    returnValue = callerFrame.locals[key];
                    break;
                }
            }
        }
        for (let i = prevDepth - 1; i >= currDepth; i--) {
            events.push({
                ...makeBaseEvent(prev, step),
                type: "function-exit",
                functionName: prev.scopeName,
                ...(returnValue !== undefined ? { returnValue } : {}),
            });
        }
    }
    return events;
}
export function detectVariableChanges(prev, curr, step) {
    if (!prev)
        return [];
    const events = [];
    const base = makeBaseEvent(curr, step);
    const prevFrame = prev.stack.stack.at(-1);
    const currFrame = curr.stack.stack.at(-1);
    if (prevFrame &&
        currFrame &&
        prev.stack.stack.length === curr.stack.stack.length) {
        for (const change of diffObject(prevFrame.locals, currFrame.locals)) {
            events.push({
                ...base,
                type: "variable-set",
                variable: change.key,
                value: change.value,
                previousValue: change.previousValue,
                scope: "local",
            });
        }
    }
    const prevStore = prev.globals.store ?? {};
    const currStore = curr.globals.store ?? {};
    for (const moduleId of Object.keys(currStore)) {
        if (moduleId === GlobalStore.INTERNAL_MODULE)
            continue;
        const prevMod = prevStore[moduleId] ?? {};
        const currMod = currStore[moduleId] ?? {};
        for (const change of diffObject(prevMod, currMod)) {
            events.push({
                ...base,
                type: "variable-set",
                variable: change.key,
                value: change.value,
                previousValue: change.previousValue,
                scope: "global",
            });
        }
    }
    return events;
}
export function detectLlmCalls(prev, curr, step) {
    if (!prev)
        return [];
    const events = [];
    const base = makeBaseEvent(curr, step);
    const prevMessages = getMessages(prev);
    const currMessages = getMessages(curr);
    if (currMessages.length <= prevMessages.length)
        return events;
    const newMessages = currMessages.slice(prevMessages.length);
    const toolCalls = [];
    let prompt = "";
    let response = "";
    for (const msg of newMessages) {
        if (msg.role === "user") {
            prompt = getMessageContent(msg);
        }
        else if (msg.role === "assistant" && msg.toolCalls) {
            for (const tc of msg.toolCalls) {
                toolCalls.push({ id: tc.id, name: tc.name, arguments: tc.arguments });
            }
        }
        else if (msg.role === "tool") {
            const pending = msg.toolCallId
                ? toolCalls.find((tc) => tc.id === msg.toolCallId && tc.result === undefined)
                : toolCalls.find((tc) => tc.result === undefined);
            if (pending) {
                pending.result = getMessageContent(msg);
                events.push({
                    ...base,
                    type: "tool-call",
                    toolName: pending.name,
                    args: pending.arguments,
                    result: pending.result,
                });
            }
        }
        else if (msg.role === "assistant") {
            response = getMessageContent(msg);
        }
    }
    events.push({
        ...base,
        type: "llm-call",
        prompt,
        response,
        toolCalls: toolCalls.map(({ name, arguments: args, result }) => ({
            name,
            arguments: args,
            result,
        })),
        tokenUsage: getTokenUsageDiff(prev, curr),
    });
    return events;
}
// Interrupt detection is not yet implemented. The checkpoint data does not
// reliably distinguish interactive interrupts from Result error-handling
// checkpoints. The InterruptThrownEvent and InterruptResolvedEvent types are
// kept for future use when interrupt metadata is available in traces.
export function detectInterrupts(_prev, _curr, _step) {
    return [];
}
export function detectBranches(prev, curr, step) {
    if (!prev)
        return [];
    const events = [];
    const base = makeBaseEvent(curr, step);
    const prevLocals = prev.stack.stack.at(-1)?.locals ?? {};
    const currLocals = curr.stack.stack.at(-1)?.locals ?? {};
    const branchDetectors = [
        {
            prefix: "__condbranch_",
            toEvent: (val) => ({ condition: val < 0 ? "else" : "if" }),
        },
        {
            prefix: "__iteration_",
            toEvent: (val) => ({ condition: "for", iteration: val }),
        },
    ];
    for (const key of Object.keys(currLocals)) {
        for (const { prefix, toEvent } of branchDetectors) {
            if (key.startsWith(prefix) && !valuesEqual(prevLocals[key], currLocals[key])) {
                events.push({ ...base, type: "branch", ...toEvent(currLocals[key]) });
            }
        }
    }
    return events;
}
const eventEmitters = [
    detectNodeTransitions,
    detectStackChanges,
    detectVariableChanges,
    detectLlmCalls,
    detectInterrupts,
    detectBranches,
];
function diff(prev, curr, step) {
    const events = [];
    for (const emitter of eventEmitters) {
        events.push(...emitter(prev, curr, step));
    }
    return events;
}
export function generateEventLog(checkpoints) {
    if (checkpoints.length === 0)
        return [];
    const events = [];
    events.push(...diff(null, checkpoints[0], 0));
    for (let i = 1; i < checkpoints.length; i++) {
        events.push(...diff(checkpoints[i - 1], checkpoints[i], i));
    }
    return events;
}
