import { isJsonObject, jsonClone } from '../types/json.js';
import { mapBridgeToolsToChat, mapChatToolsToBridge } from '../../shared/tool-mapping.js';
import { buildGeminiToolsFromBridge, prepareGeminiToolsForBridge } from '../../shared/gemini-tool-utils.js';
import { mapAnthropicToolsToChat, mapChatToolsToAnthropicTools } from '../../shared/anthropic-message-utils.js';
import { convertBridgeInputToChatMessages, convertMessagesToBridgeInput } from '../../bridge-message-utils.js';
import { resolveHubProtocolSpec } from '../policy/protocol-spec.js';
import { normalizeProviderProtocolTokenWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js';
function clampSampleRate(value) {
    const num = typeof value === 'number' && Number.isFinite(value) ? value : 1;
    if (num <= 0)
        return 0;
    if (num >= 1)
        return 1;
    return num;
}
function fnv1a32(input) {
    let hash = 0x811c9dc5;
    for (let i = 0; i < input.length; i++) {
        hash ^= input.charCodeAt(i);
        hash = (hash * 0x01000193) >>> 0;
    }
    return hash >>> 0;
}
function shouldSample(config, requestId) {
    if (!config)
        return false;
    if (config.mode === 'off')
        return false;
    const rate = clampSampleRate(config.sampleRate);
    if (rate <= 0)
        return false;
    if (rate >= 1)
        return true;
    const key = typeof requestId === 'string' && requestId.trim().length ? requestId.trim() : 'no_request_id';
    const bucket = fnv1a32(key) / 0xffffffff;
    return bucket < rate;
}
function stableStringify(value) {
    const normalize = (node) => {
        if (node === null || typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') {
            return node;
        }
        if (Array.isArray(node)) {
            return node.map((entry) => normalize(entry));
        }
        if (isJsonObject(node)) {
            const out = {};
            const keys = Object.keys(node).sort();
            for (const key of keys) {
                out[key] = normalize(node[key]);
            }
            return out;
        }
        return node;
    };
    try {
        return JSON.stringify(normalize(value));
    }
    catch {
        try {
            return JSON.stringify(value);
        }
        catch {
            return String(value);
        }
    }
}
function extractToolName(tool) {
    if (!tool || typeof tool !== 'object' || Array.isArray(tool)) {
        return undefined;
    }
    const obj = tool;
    const fnNode = obj.function && typeof obj.function === 'object' && !Array.isArray(obj.function)
        ? obj.function
        : undefined;
    const nameCandidate = fnNode?.name ?? obj.name;
    if (typeof nameCandidate === 'string' && nameCandidate.trim().length) {
        return nameCandidate.trim();
    }
    const typeRaw = typeof obj.type === 'string' ? obj.type.trim().toLowerCase() : '';
    if (typeRaw === 'web_search' || typeRaw.startsWith('web_search')) {
        return 'web_search';
    }
    return undefined;
}
function extractToolSchema(tool) {
    const name = extractToolName(tool);
    if (!name)
        return undefined;
    if (!tool || typeof tool !== 'object' || Array.isArray(tool)) {
        return { name };
    }
    const obj = tool;
    const fnNode = obj.function && typeof obj.function === 'object' && !Array.isArray(obj.function)
        ? obj.function
        : undefined;
    const description = typeof fnNode?.description === 'string'
        ? fnNode.description
        : typeof obj.description === 'string'
            ? obj.description
            : undefined;
    const parameters = fnNode && Object.prototype.hasOwnProperty.call(fnNode, 'parameters')
        ? fnNode.parameters
        : Object.prototype.hasOwnProperty.call(obj, 'parameters')
            ? obj.parameters
            : Object.prototype.hasOwnProperty.call(obj, 'input_schema')
                ? obj.input_schema
                : undefined;
    return {
        name,
        ...(description !== undefined ? { description } : {}),
        ...(parameters !== undefined ? { parameters } : {})
    };
}
function parseToolsForDiff(raw) {
    const map = new Map();
    if (!Array.isArray(raw)) {
        return map;
    }
    for (const entry of raw) {
        if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
            continue;
        }
        const obj = entry;
        const declarations = Array.isArray(obj.functionDeclarations) ? obj.functionDeclarations : undefined;
        if (declarations && declarations.length) {
            for (const decl of declarations) {
                const schema = extractToolSchema({ type: 'function', function: decl });
                if (schema && !map.has(schema.name)) {
                    map.set(schema.name, schema);
                }
            }
            continue;
        }
        const schema = extractToolSchema(entry);
        if (schema && !map.has(schema.name)) {
            map.set(schema.name, schema);
        }
    }
    return map;
}
function computeToolSchemaDiff(baselineTools, candidateTools) {
    const baseline = parseToolsForDiff(baselineTools);
    const candidate = parseToolsForDiff(candidateTools);
    const names = Array.from(new Set([...baseline.keys(), ...candidate.keys()])).sort();
    let diffCount = 0;
    const diffHead = [];
    const pushHead = (entry) => {
        if (diffHead.length < 10) {
            diffHead.push(entry);
        }
    };
    for (const name of names) {
        const a = baseline.get(name);
        const b = candidate.get(name);
        if (!a && b) {
            diffCount += 1;
            pushHead({ name, kind: 'missing_in_baseline' });
            continue;
        }
        if (a && !b) {
            diffCount += 1;
            pushHead({ name, kind: 'missing_in_candidate' });
            continue;
        }
        if (!a || !b) {
            continue;
        }
        const aSig = stableStringify({ description: a.description, parameters: a.parameters });
        const bSig = stableStringify({ description: b.description, parameters: b.parameters });
        if (aSig !== bSig) {
            diffCount += 1;
            pushHead({
                name,
                kind: 'schema_mismatch',
                baseline: jsonClone(a),
                candidate: jsonClone(b)
            });
        }
    }
    return { diffCount, diffHead };
}
function extractToolHistoryFromChatMessages(messages) {
    const out = { toolCalls: [], toolResults: [] };
    if (!Array.isArray(messages))
        return out;
    for (const msg of messages) {
        if (!msg || typeof msg !== 'object' || Array.isArray(msg))
            continue;
        const record = msg;
        const role = typeof record.role === 'string' ? record.role.trim().toLowerCase() : '';
        if (role === 'assistant') {
            const toolCalls = Array.isArray(record.tool_calls) ? record.tool_calls : [];
            for (const tc of toolCalls) {
                if (!tc || typeof tc !== 'object')
                    continue;
                const id = typeof tc.id === 'string' && tc.id.trim().length ? String(tc.id) : undefined;
                const fn = tc.function;
                const name = typeof fn?.name === 'string' ? String(fn.name).trim() : '';
                const args = fn?.arguments;
                const argsType = typeof args === 'string' ? 'string' : args && typeof args === 'object' && !Array.isArray(args) ? 'object' : 'other';
                const argsLen = typeof args === 'string' ? args.length : undefined;
                if (name) {
                    out.toolCalls.push({ id, name, argsType, argsLen });
                }
            }
        }
        if (role === 'tool') {
            const toolCallId = typeof record.tool_call_id === 'string'
                ? String(record.tool_call_id).trim()
                : typeof record.call_id === 'string'
                    ? String(record.call_id).trim()
                    : undefined;
            const content = record.content;
            const contentLen = typeof content === 'string' ? content.length : undefined;
            out.toolResults.push({ toolCallId: toolCallId || undefined, contentLen });
        }
    }
    return out;
}
function extractToolHistoryFromResponsesInputItems(input) {
    const out = { toolCalls: [], toolResults: [] };
    if (!Array.isArray(input))
        return out;
    for (const item of input) {
        if (!item || typeof item !== 'object' || Array.isArray(item))
            continue;
        const rec = item;
        const type = typeof rec.type === 'string' ? rec.type.trim().toLowerCase() : '';
        if (type === 'function_call') {
            const id = typeof rec.call_id === 'string' && String(rec.call_id).trim().length
                ? String(rec.call_id).trim()
                : typeof rec.id === 'string' && rec.id.trim().length
                    ? rec.id.trim()
                    : undefined;
            const name = typeof rec.name === 'string' ? rec.name.trim() : '';
            const args = rec.arguments;
            const argsType = typeof args === 'string' ? 'string' : args && typeof args === 'object' && !Array.isArray(args) ? 'object' : 'other';
            const argsLen = typeof args === 'string' ? args.length : undefined;
            if (name) {
                out.toolCalls.push({ id, name, argsType, argsLen });
            }
            continue;
        }
        if (type === 'function_call_output') {
            const toolCallId = typeof rec.call_id === 'string' && String(rec.call_id).trim().length
                ? String(rec.call_id).trim()
                : typeof rec.tool_call_id === 'string' && String(rec.tool_call_id).trim().length
                    ? String(rec.tool_call_id).trim()
                    : undefined;
            const output = rec.output;
            const contentLen = typeof output === 'string' ? output.length : undefined;
            out.toolResults.push({ toolCallId: toolCallId || undefined, contentLen });
            continue;
        }
    }
    return out;
}
function computeToolHistoryDiff(baseline, candidate) {
    let diffCount = 0;
    const diffs = [];
    const push = (entry) => {
        diffCount += 1;
        if (diffs.length < 10)
            diffs.push(entry);
    };
    const compareSeq = (a, b, kind) => {
        const max = Math.max(a.length, b.length);
        for (let i = 0; i < max; i += 1) {
            const ai = a[i];
            const bi = b[i];
            if (ai === undefined && bi !== undefined) {
                push({ kind, index: i, type: 'missing_in_baseline', candidate: bi });
                continue;
            }
            if (ai !== undefined && bi === undefined) {
                push({ kind, index: i, type: 'missing_in_candidate', baseline: ai });
                continue;
            }
            const aSig = stableStringify(ai);
            const bSig = stableStringify(bi);
            if (aSig !== bSig) {
                push({ kind, index: i, type: 'mismatch', baseline: ai, candidate: bi });
            }
        }
    };
    compareSeq(baseline.toolCalls, candidate.toolCalls, 'tool_calls');
    compareSeq(baseline.toolResults, candidate.toolResults, 'tool_results');
    return { diffCount, diffHead: diffs };
}
function looksLikeOpenAITools(raw) {
    if (!Array.isArray(raw))
        return false;
    return raw.some((entry) => {
        if (!entry || typeof entry !== 'object' || Array.isArray(entry))
            return false;
        const obj = entry;
        return typeof obj.type === 'string' || (obj.function && typeof obj.function === 'object');
    });
}
function looksLikeGeminiTools(raw) {
    if (!Array.isArray(raw))
        return false;
    return raw.some((entry) => {
        if (!entry || typeof entry !== 'object' || Array.isArray(entry))
            return false;
        const obj = entry;
        return Array.isArray(obj.functionDeclarations);
    });
}
function looksLikeAnthropicTools(raw) {
    if (!Array.isArray(raw))
        return false;
    return raw.some((entry) => {
        if (!entry || typeof entry !== 'object' || Array.isArray(entry))
            return false;
        const obj = entry;
        return typeof obj.name === 'string' && Object.prototype.hasOwnProperty.call(obj, 'input_schema');
    });
}
function detectToolFormat(raw) {
    if (!Array.isArray(raw))
        return 'unknown';
    if (looksLikeGeminiTools(raw))
        return 'gemini';
    if (looksLikeAnthropicTools(raw))
        return 'anthropic';
    if (looksLikeOpenAITools(raw))
        return 'openai';
    return 'unknown';
}
function convertToolDefinitions(args) {
    if (!Array.isArray(args.tools)) {
        return undefined;
    }
    const from = args.from;
    const to = args.to;
    const raw = args.tools;
    if (from === to) {
        return raw;
    }
    // All conversions pass through OpenAI-format as the canonical bridge.
    const toOpenAI = (sourceFormat, input) => {
        if (!Array.isArray(input))
            return undefined;
        if (sourceFormat === 'openai') {
            return input;
        }
        if (sourceFormat === 'gemini') {
            if (!looksLikeGeminiTools(input))
                return undefined;
            const bridgeDefs = prepareGeminiToolsForBridge(input);
            return mapBridgeToolsToChat(bridgeDefs);
        }
        if (sourceFormat === 'anthropic') {
            if (!looksLikeAnthropicTools(input))
                return undefined;
            return mapAnthropicToolsToChat(input);
        }
        return undefined;
    };
    const fromOpenAI = (targetFormat, input) => {
        if (!Array.isArray(input))
            return undefined;
        if (targetFormat === 'openai') {
            return input;
        }
        if (targetFormat === 'gemini') {
            if (!looksLikeOpenAITools(input))
                return undefined;
            const bridgeDefs = mapChatToolsToBridge(input);
            return buildGeminiToolsFromBridge(bridgeDefs);
        }
        if (targetFormat === 'anthropic') {
            if (!looksLikeOpenAITools(input))
                return undefined;
            return mapChatToolsToAnthropicTools(input);
        }
        return undefined;
    };
    const openaiTools = toOpenAI(from, raw);
    if (!openaiTools) {
        return undefined;
    }
    const converted = fromOpenAI(to, openaiTools);
    if (!converted) {
        return undefined;
    }
    return converted;
}
function resolveExpectedHistoryCarrier(providerProtocol) {
    try {
        const spec = resolveHubProtocolSpec(providerProtocol);
        return spec.toolSurface.expectedHistoryCarrier ?? null;
    }
    catch {
        return null;
    }
}
function buildCandidateTools(args) {
    const rawTools = args.tools;
    if (!Array.isArray(rawTools)) {
        return undefined;
    }
    const spec = resolveHubProtocolSpec(args.providerProtocol);
    const expected = spec.toolSurface.expectedToolFormat;
    const detected = detectToolFormat(rawTools);
    if (detected === 'unknown' || detected === expected) {
        return undefined;
    }
    const candidateTools = convertToolDefinitions({
        from: detected,
        to: expected,
        tools: rawTools
    });
    if (candidateTools === undefined) {
        return undefined;
    }
    return {
        candidateTools,
        reason: `${detected}_tools_on_${expected}_protocol`
    };
}
export function applyProviderOutboundToolSurface(args) {
    const config = args.config;
    if (!config || config.mode === 'off') {
        return args.payload;
    }
    if (!shouldSample(config, args.requestId)) {
        return args.payload;
    }
    const normalizedProviderProtocol = normalizeProviderProtocolTokenWithNative(args.providerProtocol) ?? args.providerProtocol;
    const payload = args.payload;
    const tools = payload.tools;
    const candidate = buildCandidateTools({ providerProtocol: normalizedProviderProtocol, tools });
    if (!candidate) {
        // Still allow history/tool-call surface observation even when tool definitions don't need conversion.
    }
    const stageBase = `hub_toolsurface.${config.mode}.provider_outbound`;
    const toolSchemaCandidate = candidate?.candidateTools;
    const schemaDiff = candidate ? computeToolSchemaDiff(tools, toolSchemaCandidate) : { diffCount: 0, diffHead: [] };
    const expectedHistoryCarrier = resolveExpectedHistoryCarrier(normalizedProviderProtocol);
    let historyReason = undefined;
    let historyBaseline = undefined;
    let historyCandidate = undefined;
    let historyDiff = { diffCount: 0, diffHead: [] };
    // Phase 2 (shadow): detect tool call/result history carriers drifting between
    // Chat messages and Responses input shapes.
    try {
        if (expectedHistoryCarrier === 'input') {
            const messages = payload.messages;
            const input = payload.input;
            if (Array.isArray(messages) && !Array.isArray(input)) {
                const bridge = convertMessagesToBridgeInput({
                    messages: messages,
                    tools: Array.isArray(tools) ? tools : undefined
                });
                historyReason = 'openai_chat_messages_on_responses_protocol';
                historyBaseline = extractToolHistoryFromChatMessages(messages);
                historyCandidate = extractToolHistoryFromResponsesInputItems(bridge.input);
                historyDiff = computeToolHistoryDiff(historyBaseline, historyCandidate);
            }
        }
        else if (expectedHistoryCarrier === 'messages') {
            const messages = payload.messages;
            const input = payload.input;
            if (!Array.isArray(messages) && Array.isArray(input)) {
                historyReason = 'responses_input_on_openai_chat_protocol';
                historyBaseline = extractToolHistoryFromResponsesInputItems(input);
                historyCandidate = extractToolHistoryFromChatMessages(convertBridgeInputToChatMessages({
                    input: input,
                    tools: Array.isArray(tools) ? tools : undefined
                }));
                historyDiff = computeToolHistoryDiff(historyBaseline, historyCandidate);
            }
        }
    }
    catch {
        // best-effort only
    }
    const totalDiffCount = (schemaDiff.diffCount || 0) + (historyDiff.diffCount || 0);
    const shouldRecord = totalDiffCount > 0 || Boolean(candidate?.candidateTools) || Boolean(historyReason);
    if (shouldRecord) {
        const diffHeadMerged = [];
        for (const entry of schemaDiff.diffHead) {
            if (diffHeadMerged.length >= 10)
                break;
            diffHeadMerged.push({ area: 'tool_definitions', ...entry });
        }
        for (const entry of historyDiff.diffHead) {
            if (diffHeadMerged.length >= 10)
                break;
            diffHeadMerged.push({ area: 'tool_history', ...entry });
        }
        args.stageRecorder?.record(stageBase, {
            kind: 'provider_outbound',
            providerProtocol: normalizedProviderProtocol,
            reason: candidate?.reason ?? historyReason,
            diffCount: totalDiffCount,
            diffHead: diffHeadMerged,
            ...(schemaDiff.diffCount > 0
                ? {
                    definitionDiffCount: schemaDiff.diffCount,
                    definitionDiffHead: schemaDiff.diffHead,
                    ...(tools !== undefined ? { baselineTools: jsonClone(tools) } : {}),
                    ...(toolSchemaCandidate !== undefined ? { candidateTools: jsonClone(toolSchemaCandidate) } : {})
                }
                : {}),
            ...(historyDiff.diffCount > 0
                ? {
                    historyDiffCount: historyDiff.diffCount,
                    historyDiffHead: historyDiff.diffHead,
                    historyReason,
                    historyBaseline,
                    historyCandidate
                }
                : {})
        });
    }
    if (config.mode === 'enforce') {
        let next = null;
        const ensureClone = () => {
            if (!next) {
                next = jsonClone(payload);
            }
            return next;
        };
        if (candidate && candidate.candidateTools !== undefined) {
            ensureClone().tools = candidate.candidateTools;
        }
        // Best-effort: normalize history carrier when we can reconstruct a canonical representation.
        try {
            if (expectedHistoryCarrier === 'input') {
                const messages = payload.messages;
                const input = payload.input;
                if (Array.isArray(messages) && !Array.isArray(input)) {
                    const bridge = convertMessagesToBridgeInput({
                        messages: messages,
                        tools: Array.isArray(tools) ? tools : undefined
                    });
                    ensureClone().input = bridge.input;
                    try {
                        delete ensureClone().messages;
                    }
                    catch {
                        ensureClone().messages = undefined;
                    }
                }
            }
            else if (expectedHistoryCarrier === 'messages') {
                const messages = payload.messages;
                const input = payload.input;
                if (!Array.isArray(messages) && Array.isArray(input)) {
                    const convertedMessages = convertBridgeInputToChatMessages({
                        input: input,
                        tools: Array.isArray(tools) ? tools : undefined
                    });
                    ensureClone().messages = convertedMessages;
                    try {
                        delete ensureClone().input;
                    }
                    catch {
                        ensureClone().input = undefined;
                    }
                }
            }
        }
        catch {
            // best-effort only
        }
        return next ?? payload;
    }
    return payload;
}
