/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { NodeType } from './types.js';
import { createHash } from 'node:crypto';
import { debugLogger } from '../../utils/debugLogger.js';
import { isSnapshotState } from '../utils/snapshotGenerator.js';
import { deriveStableId } from '../../utils/cryptoUtils.js';
import { ensureStableToolIds } from '../../utils/sessionUtils.js';
// Global WeakMap to cache hashes for Part objects.
// This optimizes getStableId by avoiding redundant stringify/hash operations
// on the same object instances across multiple management passes.
const PART_HASH_CACHE = new WeakMap();
function isTextPart(part) {
    return typeof part.text === 'string';
}
function isInlineDataPart(part) {
    return (typeof part.inlineData === 'object' &&
        part.inlineData !== null &&
        typeof part.inlineData.data === 'string');
}
function isFileDataPart(part) {
    return (typeof part.fileData === 'object' &&
        part.fileData !== null &&
        typeof part.fileData.fileUri === 'string');
}
function isFunctionCallPart(part) {
    return (typeof part.functionCall === 'object' &&
        part.functionCall !== null &&
        typeof part.functionCall.name === 'string');
}
function isFunctionResponsePart(part) {
    return (typeof part.functionResponse === 'object' &&
        part.functionResponse !== null &&
        typeof part.functionResponse.name === 'string');
}
function isExecutableCodePart(part) {
    return (typeof part.executableCode === 'object' &&
        part.executableCode !== null &&
        typeof part.executableCode.code === 'string' &&
        typeof part.executableCode.language === 'string');
}
function isCodeExecutionResultPart(part) {
    return (typeof part.codeExecutionResult === 'object' &&
        part.codeExecutionResult !== null &&
        typeof part.codeExecutionResult.output === 'string' &&
        typeof part.codeExecutionResult.outcome === 'string');
}
/**
 * Generates a stable ID for an object reference using a NodeIdService.
 * Falls back to content-based hashing for Part-like objects to ensure
 * stability across object re-creations (e.g. during history mapping).
 */
export function getStableId(obj, idService, turnSalt = '', partIdx = 0) {
    let id = idService.get(obj);
    if (id)
        return id;
    const cachedHash = PART_HASH_CACHE.get(obj);
    if (cachedHash) {
        id = `${cachedHash}_${turnSalt}_${partIdx}`;
        idService.set(obj, id);
        return id;
    }
    const part = obj;
    let contentHash;
    if (isTextPart(part)) {
        contentHash = createHash('sha256').update(part.text).digest('hex');
        id = `text_${contentHash}_${turnSalt}_${partIdx}`;
    }
    else if (isInlineDataPart(part)) {
        contentHash = createHash('sha256')
            .update(part.inlineData.data)
            .digest('hex');
        id = `media_${contentHash}_${turnSalt}_${partIdx}`;
    }
    else if (isFileDataPart(part)) {
        contentHash = createHash('sha256')
            .update(part.fileData.fileUri)
            .digest('hex');
        id = `file_${contentHash}_${turnSalt}_${partIdx}`;
    }
    else if (isFunctionCallPart(part)) {
        if (part.functionCall.id) {
            id = `call_${part.functionCall.id}`;
        }
        else {
            contentHash = createHash('sha256')
                .update(`call:${part.functionCall.name}:${JSON.stringify(part.functionCall.args)}`)
                .digest('hex');
            id = `call_h_${contentHash}_${turnSalt}_${partIdx}`;
        }
    }
    else if (isFunctionResponsePart(part)) {
        if (part.functionResponse.id) {
            id = `resp_${part.functionResponse.id}`;
        }
        else {
            contentHash = createHash('sha256')
                .update(`resp:${part.functionResponse.name}:${JSON.stringify(part.functionResponse.response)}`)
                .digest('hex');
            id = `resp_h_${contentHash}_${turnSalt}_${partIdx}`;
        }
    }
    else if (isExecutableCodePart(part)) {
        contentHash = createHash('sha256')
            .update(`exec:${part.executableCode.language}:${part.executableCode.code}`)
            .digest('hex');
        id = `exec_${contentHash}_${turnSalt}_${partIdx}`;
    }
    else if (isCodeExecutionResultPart(part)) {
        contentHash = createHash('sha256')
            .update(`result:${part.codeExecutionResult.outcome}:${part.codeExecutionResult.output}`)
            .digest('hex');
        id = `result_${contentHash}_${turnSalt}_${partIdx}`;
    }
    if (contentHash) {
        PART_HASH_CACHE.set(obj, contentHash);
    }
    if (!id) {
        if (turnSalt && partIdx === -1) {
            id = `turn_${turnSalt}`;
        }
        else {
            id = `${turnSalt}_f_${partIdx}`;
        }
    }
    idService.set(obj, id);
    return id;
}
/**
 * Builds a 1:1 Mirror Graph from Chat History.
 * Every Part in history is mapped to exactly one ConcreteNode.
 */
export class ContextGraphBuilder {
    idService;
    constructor(idService) {
        this.idService = idService;
    }
    processHistory(history) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
        ensureStableToolIds(history);
        const nodes = [];
        for (let turnIdx = 0; turnIdx < history.length; turnIdx++) {
            const turn = history[turnIdx];
            const msg = turn.content;
            if (!msg.parts)
                continue;
            const hasEnvHeader = msg.parts?.some((p) => isTextPart(p) && p.text.trim().startsWith('<session_context>'));
            const turnSalt = hasEnvHeader && turnIdx === 0
                ? deriveStableId(['environment-context'])
                : turn.id;
            const turnId = turnSalt.startsWith('turn_')
                ? turnSalt
                : `turn_${turnSalt}`;
            if (msg.role === 'user') {
                for (let partIdx = 0; partIdx < msg.parts.length; partIdx++) {
                    const part = msg.parts[partIdx];
                    // Skip legacy session context headers if they appear later in history (after Turn 0).
                    if (isTextPart(part) &&
                        part.text.trim().startsWith('<session_context>') &&
                        turnIdx > 0) {
                        debugLogger.log('[ContextGraphBuilder] Skipping legacy environment header turn from graph.');
                        continue;
                    }
                    const isSnapshot = isTextPart(part) && isSnapshotState(part.text);
                    const id = getStableId(part, this.idService, turnSalt, partIdx);
                    const node = {
                        id,
                        timestamp: Date.now(),
                        type: isFunctionResponsePart(part)
                            ? NodeType.TOOL_EXECUTION
                            : isSnapshot
                                ? NodeType.SNAPSHOT
                                : NodeType.USER_PROMPT,
                        role: 'user',
                        payload: part,
                        turnId,
                    };
                    nodes.push(node);
                }
            }
            else if (msg.role === 'model') {
                for (let partIdx = 0; partIdx < msg.parts.length; partIdx++) {
                    const part = msg.parts[partIdx];
                    const id = getStableId(part, this.idService, turnSalt, partIdx);
                    const node = {
                        id,
                        timestamp: Date.now(),
                        type: isFunctionCallPart(part)
                            ? NodeType.TOOL_EXECUTION
                            : NodeType.AGENT_THOUGHT,
                        role: 'model',
                        payload: part,
                        turnId,
                    };
                    nodes.push(node);
                }
            }
        }
        debugLogger.log(`[ContextGraphBuilder] Mirror Graph built with ${nodes.length} nodes.`);
        return nodes;
    }
}
//# sourceMappingURL=toGraph.js.map