// metadata-cache.ts - Persistent MCP metadata cache
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { getAgentPath } from "./agent-dir.js";
import { createHash } from "node:crypto";
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
import { formatToolName, isToolExcluded } from "./types.js";
import { resourceNameToToolName } from "./resource-tools.js";
import { extractToolUiStreamMode, interpolateEnvRecord, resolveBearerToken, resolveConfigPath } from "./utils.js";
const CACHE_VERSION = 1;
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
export function getMetadataCachePath() {
    return getAgentPath("mcp-cache.json");
}
export function loadMetadataCache() {
    const cachePath = getMetadataCachePath();
    if (!existsSync(cachePath))
        return null;
    try {
        const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
        if (!raw || typeof raw !== "object")
            return null;
        if (raw.version !== CACHE_VERSION)
            return null;
        if (!raw.servers || typeof raw.servers !== "object")
            return null;
        return raw;
    }
    catch {
        return null;
    }
}
export function saveMetadataCache(cache) {
    const cachePath = getMetadataCachePath();
    const dir = dirname(cachePath);
    mkdirSync(dir, { recursive: true });
    let merged = { version: CACHE_VERSION, servers: {} };
    try {
        if (existsSync(cachePath)) {
            const existing = JSON.parse(readFileSync(cachePath, "utf-8"));
            if (existing && existing.version === CACHE_VERSION && existing.servers) {
                merged.servers = { ...existing.servers };
            }
        }
    }
    catch {
        // Ignore parse errors and proceed with empty cache
    }
    merged.version = CACHE_VERSION;
    merged.servers = { ...merged.servers, ...cache.servers };
    const tmpPath = `${cachePath}.${process.pid}.tmp`;
    writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
    renameSync(tmpPath, cachePath);
}
export function computeServerHash(definition) {
    // Hash only fields that affect server identity and tool/resource output.
    // Exclude lifecycle, idleTimeout, debug — those are runtime behavior settings
    // that don't change which tools a server exposes.
    const identity = {
        command: definition.command,
        args: definition.args,
        env: interpolateEnvRecord(definition.env),
        cwd: resolveConfigPath(definition.cwd),
        url: definition.url,
        headers: interpolateEnvRecord(definition.headers),
        auth: definition.auth,
        bearerToken: resolveBearerToken(definition),
        bearerTokenEnv: definition.bearerTokenEnv,
        exposeResources: definition.exposeResources,
        excludeTools: definition.excludeTools,
    };
    const normalized = stableStringify(identity);
    return createHash("sha256").update(normalized).digest("hex");
}
export function isServerCacheValid(entry, definition, maxAgeMs = CACHE_MAX_AGE_MS) {
    if (!entry || entry.configHash !== computeServerHash(definition))
        return false;
    if (!entry.cachedAt || typeof entry.cachedAt !== "number")
        return false;
    if (maxAgeMs > 0 && Date.now() - entry.cachedAt > maxAgeMs)
        return false;
    return true;
}
export function reconstructToolMetadata(serverName, entry, prefix, definition) {
    const metadata = [];
    for (const tool of entry.tools ?? []) {
        if (!tool?.name)
            continue;
        if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) {
            continue;
        }
        metadata.push({
            name: formatToolName(tool.name, serverName, prefix),
            originalName: tool.name,
            description: tool.description ?? "",
            inputSchema: tool.inputSchema,
            uiResourceUri: tool.uiResourceUri,
            uiStreamMode: tool.uiStreamMode,
        });
    }
    if (definition.exposeResources !== false) {
        for (const resource of entry.resources ?? []) {
            if (!resource?.name || !resource?.uri)
                continue;
            const baseName = `get_${resourceNameToToolName(resource.name)}`;
            if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) {
                continue;
            }
            metadata.push({
                name: formatToolName(baseName, serverName, prefix),
                originalName: baseName,
                description: resource.description ?? `Read resource: ${resource.uri}`,
                resourceUri: resource.uri,
            });
        }
    }
    return metadata;
}
export function serializeTools(tools) {
    return tools
        .filter(t => t?.name)
        .map(t => ({
        name: t.name,
        description: t.description,
        inputSchema: t.inputSchema,
        uiResourceUri: tryGetToolUiResourceUri(t),
        uiStreamMode: extractToolUiStreamMode(t._meta),
    }));
}
export function serializeResources(resources) {
    return resources
        .filter(r => r?.name && r?.uri)
        .map(r => ({
        uri: r.uri,
        name: r.name,
        description: r.description,
    }));
}
function stableStringify(value) {
    if (value === null || value === undefined || typeof value !== "object") {
        const serialized = JSON.stringify(value);
        return serialized === undefined ? "undefined" : serialized;
    }
    if (Array.isArray(value)) {
        return `[${value.map(v => stableStringify(v)).join(",")}]`;
    }
    const obj = value;
    const keys = Object.keys(obj).sort();
    return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
}
function tryGetToolUiResourceUri(tool) {
    try {
        return getToolUiResourceUri({ _meta: tool._meta });
    }
    catch {
        return undefined;
    }
}
