import { createCallResult } from './result-utils.js';
import { readSchemaCache, writeSchemaCache } from './schema-cache.js';
const KNOWN_OPTION_KEYS = new Set(['tailLog', 'timeout', 'stream', 'streamLog', 'mimeType', 'metadata', 'log']);
// defaultToolNameMapper converts camelCase property access into kebab-case tool names.
function defaultToolNameMapper(propertyKey) {
    if (typeof propertyKey !== 'string') {
        throw new TypeError('Tool name must be a string when using server proxy.');
    }
    return propertyKey.replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase();
}
// canonicalizeToolName strips punctuation for loose matching of tool names.
function canonicalizeToolName(name) {
    return name.toLowerCase().replace(/[^a-z0-9]/g, '');
}
// isPlainObject narrows unknown values to plain object records.
function isPlainObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
// createToolSchemaInfo normalizes schema metadata used for argument mapping.
function createToolSchemaInfo(schemaRaw) {
    if (!schemaRaw || typeof schemaRaw !== 'object') {
        return undefined;
    }
    const schema = schemaRaw;
    const propertiesRaw = schema.properties;
    const propertyKeys = propertiesRaw && typeof propertiesRaw === 'object' ? Object.keys(propertiesRaw) : [];
    const requiredKeys = Array.isArray(schema.required) ? schema.required : [];
    const orderedKeys = [];
    const seen = new Set();
    for (const key of requiredKeys) {
        if (typeof key === 'string' && !seen.has(key)) {
            orderedKeys.push(key);
            seen.add(key);
        }
    }
    for (const key of propertyKeys) {
        if (!seen.has(key)) {
            orderedKeys.push(key);
            seen.add(key);
        }
    }
    return {
        schema,
        orderedKeys,
        requiredKeys,
        propertySet: new Set([...propertyKeys, ...requiredKeys]),
    };
}
// applyDefaults merges JSON-schema default values into the provided arguments.
function applyDefaults(meta, args) {
    const propertiesRaw = meta.schema.properties;
    if (!propertiesRaw || typeof propertiesRaw !== 'object') {
        return args;
    }
    const result = isPlainObject(args) ? { ...args } : {};
    for (const [key, value] of Object.entries(propertiesRaw)) {
        if (value &&
            typeof value === 'object' &&
            'default' in value &&
            result[key] === undefined) {
            result[key] = value.default;
        }
    }
    if (Object.keys(result).length === 0 && !isPlainObject(args)) {
        return args;
    }
    return result;
}
// validateRequired ensures all schema-required fields are present before invocation.
function validateRequired(meta, args) {
    if (meta.requiredKeys.length === 0) {
        return;
    }
    if (!isPlainObject(args)) {
        throw new Error(`Missing required arguments: ${meta.requiredKeys.join(', ')}`);
    }
    const missing = meta.requiredKeys.filter((key) => args[key] === undefined);
    if (missing.length > 0) {
        throw new Error(`Missing required arguments: ${missing.join(', ')}`);
    }
}
// createServerProxy returns a proxy that maps property access to MCP tool invocations.
export function createServerProxy(runtime, serverName, mapOrOptions, maybeOptions) {
    let mapPropertyToTool = defaultToolNameMapper;
    let options;
    if (typeof mapOrOptions === 'function') {
        mapPropertyToTool = mapOrOptions;
        options = maybeOptions;
    }
    else if (mapOrOptions) {
        options = mapOrOptions;
        if (typeof mapOrOptions.mapPropertyToTool === 'function') {
            mapPropertyToTool = mapOrOptions.mapPropertyToTool;
        }
    }
    const cacheSchemas = options?.cacheSchemas ?? true;
    const initialSchemas = options?.initialSchemas ?? undefined;
    const toolSchemaCache = new Map();
    const persistedSchemas = new Map();
    const toolAliasMap = new Map();
    let schemaFetch = null;
    let diskLoad = null;
    let persistPromise = null;
    let refreshPending = false;
    let definitionForCache;
    if (cacheSchemas) {
        try {
            definitionForCache = runtime.getDefinition(serverName);
        }
        catch {
            definitionForCache = undefined;
        }
    }
    if (cacheSchemas && !initialSchemas && definitionForCache) {
        diskLoad = loadSchemasFromDisk(definitionForCache);
        refreshPending = true;
    }
    if (initialSchemas) {
        for (const [key, schemaRaw] of Object.entries(initialSchemas)) {
            storeSchema(key, schemaRaw);
        }
        persistPromise = persistSchemas();
    }
    // consumePersist waits for any in-flight disk persistence to finish before reading from cache maps.
    async function consumePersist() {
        if (!persistPromise) {
            return;
        }
        try {
            await persistPromise;
        }
        finally {
            persistPromise = null;
        }
    }
    // ensureMetadata loads schema information for the requested tool, optionally refreshing from the server.
    async function ensureMetadata(toolName) {
        await consumePersist();
        const cached = toolSchemaCache.get(toolName);
        if (cached && !refreshPending) {
            return cached;
        }
        if (diskLoad) {
            try {
                await diskLoad;
            }
            finally {
                diskLoad = null;
            }
            if (toolSchemaCache.has(toolName) && !refreshPending) {
                return toolSchemaCache.get(toolName);
            }
        }
        if (!schemaFetch) {
            schemaFetch = runtime
                .listTools(serverName, { includeSchema: true })
                .then((tools) => {
                for (const tool of tools) {
                    if (!tool.inputSchema || typeof tool.inputSchema !== 'object') {
                        continue;
                    }
                    storeSchema(tool.name, tool.inputSchema);
                }
                persistPromise = persistSchemas();
                refreshPending = false;
            })
                .catch((error) => {
                schemaFetch = null;
                throw error;
            });
        }
        await schemaFetch;
        await consumePersist();
        return toolSchemaCache.get(toolName);
    }
    // storeSchema caches schema info locally and records aliases for lookup.
    function storeSchema(key, schemaRaw) {
        const info = createToolSchemaInfo(schemaRaw);
        if (!info) {
            return;
        }
        const canonical = mapPropertyToTool(key);
        toolSchemaCache.set(canonical, info);
        if (canonical !== key) {
            toolSchemaCache.set(key, info);
        }
        const canonicalAlias = canonicalizeToolName(key);
        if (!toolAliasMap.has(canonicalAlias)) {
            toolAliasMap.set(canonicalAlias, key);
        }
        const mapperAlias = canonicalizeToolName(canonical);
        if (!toolAliasMap.has(mapperAlias)) {
            toolAliasMap.set(mapperAlias, key);
        }
        if (cacheSchemas && definitionForCache && isPlainObject(schemaRaw)) {
            persistedSchemas.set(canonical, schemaRaw);
        }
    }
    // loadSchemasFromDisk hydrates the in-memory cache from the persisted schema snapshot.
    async function loadSchemasFromDisk(definition) {
        try {
            const snapshot = await readSchemaCache(definition);
            if (!snapshot) {
                return;
            }
            for (const [key, schemaRaw] of Object.entries(snapshot.tools)) {
                storeSchema(key, schemaRaw);
            }
        }
        catch {
            // ignore cache read failures
        }
    }
    // persistSchemas writes cached schema data to disk when enabled.
    function persistSchemas() {
        if (!cacheSchemas || !definitionForCache || persistedSchemas.size === 0) {
            return null;
        }
        const definition = definitionForCache;
        const snapshot = {
            updatedAt: new Date().toISOString(),
            tools: Object.fromEntries(persistedSchemas.entries()),
        };
        return writeSchemaCache(definition, snapshot).catch(() => {
            // best-effort persistence
        });
    }
    const base = {
        call: async (toolName, options) => {
            const result = await runtime.callTool(serverName, toolName, options ?? {});
            return createCallResult(result);
        },
        listTools: (options) => runtime.listTools(serverName, options),
    };
    return new Proxy(base, {
        get(target, property, receiver) {
            if (Reflect.has(target, property)) {
                return Reflect.get(target, property, receiver);
            }
            const propertyKey = property;
            const canonicalKey = typeof propertyKey === 'string' ? canonicalizeToolName(propertyKey) : null;
            let resolvedToolName = typeof propertyKey === 'string' && canonicalKey
                ? (toolAliasMap.get(canonicalKey) ?? mapPropertyToTool(propertyKey))
                : mapPropertyToTool(propertyKey);
            return async (...callArgs) => {
                let schemaInfo;
                try {
                    schemaInfo = await ensureMetadata(resolvedToolName);
                }
                catch {
                    schemaInfo = undefined;
                }
                if (typeof propertyKey === 'string' && canonicalKey) {
                    const alias = toolAliasMap.get(canonicalKey);
                    if (alias && alias !== resolvedToolName) {
                        resolvedToolName = alias;
                        try {
                            schemaInfo = await ensureMetadata(resolvedToolName);
                        }
                        catch {
                            // ignore and keep prior schema if available
                        }
                    }
                }
                const positional = [];
                const argsAccumulator = {};
                const optionsAccumulator = {};
                for (const arg of callArgs) {
                    if (isPlainObject(arg)) {
                        const keys = Object.keys(arg);
                        const treatAsArgs = schemaInfo !== undefined &&
                            keys.length > 0 &&
                            (keys.every((key) => schemaInfo.propertySet.has(key)) ||
                                keys.every((key) => !KNOWN_OPTION_KEYS.has(key)));
                        if (treatAsArgs) {
                            Object.assign(argsAccumulator, arg);
                        }
                        else {
                            Object.assign(optionsAccumulator, arg);
                        }
                    }
                    else {
                        positional.push(arg);
                    }
                }
                const explicitArgs = optionsAccumulator.args;
                if (explicitArgs !== undefined) {
                    delete optionsAccumulator.args;
                }
                const finalOptions = { ...optionsAccumulator };
                let combinedArgs = explicitArgs;
                if (schemaInfo) {
                    const schema = schemaInfo;
                    if (positional.length > schema.orderedKeys.length) {
                        throw new Error(`Too many positional arguments for tool "${resolvedToolName}"`);
                    }
                    if (positional.length > 0) {
                        const baseArgs = isPlainObject(combinedArgs) ? { ...combinedArgs } : {};
                        positional.forEach((value, idx) => {
                            const key = schema.orderedKeys[idx];
                            if (key) {
                                baseArgs[key] = value;
                            }
                        });
                        combinedArgs = baseArgs;
                    }
                    if (Object.keys(argsAccumulator).length > 0) {
                        const baseArgs = isPlainObject(combinedArgs) ? { ...combinedArgs } : {};
                        Object.assign(baseArgs, argsAccumulator);
                        combinedArgs = baseArgs;
                    }
                    if (combinedArgs !== undefined) {
                        combinedArgs = applyDefaults(schema, combinedArgs);
                    }
                    else {
                        const defaults = applyDefaults(schema, undefined);
                        if (defaults && typeof defaults === 'object') {
                            combinedArgs = defaults;
                        }
                    }
                    validateRequired(schema, combinedArgs);
                }
                else {
                    if (positional.length > 0) {
                        combinedArgs = positional;
                    }
                    if (Object.keys(argsAccumulator).length > 0) {
                        const baseArgs = isPlainObject(combinedArgs) ? { ...combinedArgs } : {};
                        Object.assign(baseArgs, argsAccumulator);
                        combinedArgs = baseArgs;
                    }
                }
                if (combinedArgs !== undefined) {
                    finalOptions.args = combinedArgs;
                }
                const result = await runtime.callTool(serverName, resolvedToolName, finalOptions);
                return createCallResult(result);
            };
        },
    });
}
//# sourceMappingURL=server-proxy.js.map