import { isCopilotGeminiModel } from "./copilot-gemini-payload-sanitizer.js";
import { unflattenArgumentsWithSchema } from "./flattened-tool-arguments.js";
function isPlainObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function schemaTypeIncludes(schema, type) {
    if (schema.type === type)
        return true;
    return Array.isArray(schema.type) && schema.type.includes(type);
}
function isArraySchema(schema) {
    if (!isPlainObject(schema))
        return false;
    if (schemaTypeIncludes(schema, "array"))
        return true;
    if ("items" in schema && !schemaTypeIncludes(schema, "object"))
        return true;
    const union = schema.anyOf ?? schema.oneOf;
    return Array.isArray(union) && union.some((branch) => isArraySchema(branch));
}
function requiredArrayPropertyNames(schema) {
    if (!isPlainObject(schema))
        return [];
    const required = schema.required;
    const properties = schema.properties;
    if (!Array.isArray(required) || !isPlainObject(properties))
        return [];
    return required.filter((name) => (typeof name === "string" &&
        Object.hasOwn(properties, name) &&
        isArraySchema(properties[name])));
}
function fillMissingRequiredArrayProperties(args, schema) {
    const missing = requiredArrayPropertyNames(schema).filter((name) => !Object.hasOwn(args, name));
    if (missing.length === 0)
        return args;
    const next = { ...args };
    for (const name of missing)
        next[name] = [];
    return next;
}
/**
 * Reconstruct flattened Gemini tool-call arguments into proper nested
 * arrays/objects. Returns the original reference unchanged when there is nothing
 * to reconstruct. Bracket-indexed keys are always reconstructed; purely dotted
 * keys are reconstructed only when the optional `schema` marks their head
 * segment as an object/array container property. Reconstruction (and its
 * prototype-pollution guard) is delegated to the shared canonical helper.
 */
export function unflattenGeminiToolArguments(args, schema) {
    if (!isPlainObject(args))
        return args;
    const reconstructed = unflattenArgumentsWithSchema(args, schema);
    return isPlainObject(reconstructed)
        ? fillMissingRequiredArrayProperties(reconstructed, schema)
        : reconstructed;
}
/**
 * If `model` is a GitHub Copilot Gemini model, normalize flattened tool-call
 * arguments; otherwise return them unchanged. Used to gate
 * {@link unflattenGeminiToolArguments} by model at tool-call time. The optional
 * `schema` is the tool's parameter schema, used to disambiguate dotted keys.
 */
export function normalizeToolArgumentsForModel(args, model, schema) {
    if (!model || !isCopilotGeminiModel(model))
        return args;
    return unflattenGeminiToolArguments(args, schema);
}
/** Map each tool name in an OpenAI chat-completions payload to its parameter schema. */
function toolParameterSchemas(tools) {
    const schemas = new Map();
    if (!Array.isArray(tools))
        return schemas;
    for (const tool of tools) {
        if (!isPlainObject(tool))
            continue;
        // OpenAI chat-completions tool shape: { type: "function", function: { name, parameters } }.
        const fn = tool.function;
        if (isPlainObject(fn) && typeof fn.name === "string") {
            schemas.set(fn.name, fn.parameters);
            continue;
        }
        // Defensive: flat tool shape { name, parameters }.
        if (typeof tool.name === "string")
            schemas.set(tool.name, tool.parameters);
    }
    return schemas;
}
/**
 * Reconstruct flattened GitHub Copilot Gemini tool-call arguments on the
 * **outbound replay payload**, so prior assistant tool calls are sent back to
 * CAPI in the nested array/object shape Gemini originally produced.
 *
 * Why this exists
 * ---------------
 * {@link normalizeToolArgumentsForModel} only unflattens at tool *execution*
 * time; the persisted assistant message keeps the raw flattened arguments CAPI
 * delivered (for example `{ "edits[0].newText": "..." }`). When that message is
 * replayed on the next turn, CAPI parses those literal keys straight into the
 * Gemini `FunctionCall.Args`, producing a function call that does not match the
 * tool's declared schema (nor the structure Gemini signed). Gemini then ends
 * the turn with `MALFORMED_FUNCTION_CALL` / `UNEXPECTED_TOOL_CALL` / `OTHER`,
 * which CAPI surfaces as a bare `finish_reason: "error"` — so multi-turn tool
 * use dies one turn after any array/object tool call (such as `edit`).
 *
 * This rewrites each replayed assistant `tool_calls[].function.arguments` JSON
 * into the reconstructed nested shape (reusing {@link unflattenGeminiToolArguments}
 * with the tool's own parameter schema, looked up from the payload's `tools`),
 * fixing both new and already-persisted sessions. Gated to GitHub Copilot Gemini
 * models, fail-open on non-JSON arguments, and a no-op for well-formed args.
 */
export function normalizeCopilotGeminiReplayToolArguments(payload, model) {
    if (!isCopilotGeminiModel(model))
        return payload;
    if (!isPlainObject(payload))
        return payload;
    const messages = payload.messages;
    if (!Array.isArray(messages))
        return payload;
    const schemas = toolParameterSchemas(payload.tools);
    let mutated = false;
    const nextMessages = messages.map((message) => {
        if (!isPlainObject(message) || message.role !== "assistant")
            return message;
        const toolCalls = message.tool_calls;
        if (!Array.isArray(toolCalls) || toolCalls.length === 0)
            return message;
        let messageMutated = false;
        const nextToolCalls = toolCalls.map((toolCall) => {
            if (!isPlainObject(toolCall))
                return toolCall;
            const fn = toolCall.function;
            if (!isPlainObject(fn) || typeof fn.arguments !== "string")
                return toolCall;
            let parsed;
            try {
                parsed = JSON.parse(fn.arguments);
            }
            catch {
                return toolCall; // fail open: never corrupt a replayed argument string
            }
            if (!isPlainObject(parsed))
                return toolCall;
            const schema = typeof fn.name === "string" ? schemas.get(fn.name) : undefined;
            const reconstructed = unflattenGeminiToolArguments(parsed, schema);
            if (reconstructed === parsed)
                return toolCall;
            messageMutated = true;
            return { ...toolCall, function: { ...fn, arguments: JSON.stringify(reconstructed) } };
        });
        if (!messageMutated)
            return message;
        mutated = true;
        return { ...message, tool_calls: nextToolCalls };
    });
    if (!mutated)
        return payload;
    return { ...payload, messages: nextMessages };
}
//# sourceMappingURL=copilot-gemini-tool-arguments.js.map