"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonCompressor = void 0;
class JsonCompressor {
    deduplicationMap = new Map();
    objectSeenCount = new Map();
    propertyNamesMap = new Map();
    propertyNamesSeenCount = new Map();
    compressionThreshold;
    maxTokens;
    toolName;
    storage;
    constructor(storage, compressionThreshold, maxTokens, toolName) {
        this.storage = storage;
        this.compressionThreshold = compressionThreshold;
        this.maxTokens = maxTokens;
        this.toolName = toolName;
    }
    clearDeduplication() {
        this.deduplicationMap.clear();
        this.objectSeenCount.clear();
        this.propertyNamesMap.clear();
        this.propertyNamesSeenCount.clear();
    }
    updateSettings(compressionThreshold, maxTokens) {
        this.compressionThreshold = compressionThreshold;
        this.maxTokens = maxTokens;
    }
    tryParseJson(content) {
        try {
            const parsed = JSON.parse(content);
            if (typeof parsed === 'string') {
                try {
                    return this.tryParseJson(parsed);
                }
                catch (e) {
                    return parsed;
                }
            }
            if (parsed &&
                typeof parsed === 'object' &&
                Array.isArray(parsed.content) &&
                parsed.content.length > 0) {
                const firstContent = parsed.content[0];
                if (firstContent.type === 'text' && typeof firstContent.text === 'string') {
                    try {
                        const nestedData = this.tryParseJson(firstContent.text);
                        return {
                            _mcp_format: true,
                            _raw_structure: { content: [{ type: 'text' }] },
                            _data: nestedData
                        };
                    }
                    catch (e) {
                        return parsed;
                    }
                }
            }
            return parsed;
        }
        catch {
            return null;
        }
    }
    generateSchema(obj, maxDepth = 3, currentDepth = 0) {
        if (currentDepth > maxDepth) {
            return { type: 'any' };
        }
        if (obj && typeof obj === 'object' && obj._mcp_format === true && obj._data) {
            const dataSchema = this.generateSchema(obj._data, maxDepth, currentDepth);
            return {
                type: 'mcp_response',
                properties: {
                    _data: dataSchema
                }
            };
        }
        if (obj === null) {
            return { type: 'null' };
        }
        if (Array.isArray(obj)) {
            if (obj.length === 0) {
                return { type: 'array', items: { type: 'unknown' } };
            }
            const sample = obj.slice(0, 3);
            const itemSchemas = sample.map(item => this.generateSchema(item, maxDepth, currentDepth + 1));
            return { type: 'array', items: itemSchemas[0] };
        }
        if (typeof obj === 'object') {
            const properties = {};
            for (const [key, value] of Object.entries(obj)) {
                properties[key] = this.generateSchema(value, maxDepth, currentDepth + 1);
            }
            return { type: 'object', properties };
        }
        return { type: typeof obj };
    }
    calculateSimilarity(str1, str2) {
        const maxLen = Math.max(str1.length, str2.length);
        if (maxLen === 0)
            return 1.0;
        let commonPrefixLen = 0;
        const minLen = Math.min(str1.length, str2.length);
        for (let i = 0; i < minLen; i++) {
            if (str1[i] === str2[i]) {
                commonPrefixLen++;
            }
            else {
                break;
            }
        }
        return commonPrefixLen / maxLen;
    }
    detectLowSignalProperties(obj) {
        if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
            return { lowSignal: [], metadata: {} };
        }
        const lowSignal = [];
        const metadata = {};
        const entries = Object.entries(obj);
        const urlPattern = /^https?:\/\//;
        const urlProps = [];
        for (const [key, value] of entries) {
            if (typeof value === 'string' && urlPattern.test(value)) {
                urlProps.push(key);
            }
        }
        if (urlProps.length >= 3) {
            const urlValues = urlProps.map(key => obj[key]);
            let totalSimilarity = 0;
            let comparisons = 0;
            for (let i = 0; i < urlValues.length - 1; i++) {
                for (let j = i + 1; j < urlValues.length; j++) {
                    totalSimilarity += this.calculateSimilarity(urlValues[i], urlValues[j]);
                    comparisons++;
                }
            }
            const avgSimilarity = comparisons > 0 ? totalSimilarity / comparisons : 0;
            if (avgSimilarity > 0.6) {
                lowSignal.push(...urlProps);
                metadata.url_similarity = avgSimilarity;
                metadata.url_count = urlProps.length;
            }
        }
        const lowSignalPatterns = [/_url$/, /_id$/, /^node_id$/, /^avatar_url$/, /^gravatar_id$/];
        for (const [key, value] of entries) {
            if (lowSignalPatterns.some(pattern => pattern.test(key)) && !lowSignal.includes(key)) {
                lowSignal.push(key);
            }
        }
        return { lowSignal, metadata };
    }
    compressObjectWithLowSignalDetection(obj, path = "") {
        if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
            return obj;
        }
        const { lowSignal, metadata } = this.detectLowSignalProperties(obj);
        if (lowSignal.length < 5) {
            return obj;
        }
        const highSignal = {};
        const compressed = {};
        for (const [key, value] of Object.entries(obj)) {
            if (lowSignal.includes(key)) {
                compressed[key] = value;
            }
            else {
                highSignal[key] = value;
            }
        }
        const compressedHash = this.hashObject(compressed);
        let compressedKey = this.deduplicationMap.get(compressedHash);
        if (!compressedKey) {
            compressedKey = this.storage.generateKey();
            this.deduplicationMap.set(compressedHash, compressedKey);
            const compressionMetadata = {
                compressed_properties: compressed,
                compression_reason: 'low_signal_detection',
                similarity_score: metadata.url_similarity,
            };
            this.storage.storeString(compressedKey, JSON.stringify(compressionMetadata));
        }
        const propertyNamesHash = this.hashObject(lowSignal);
        const propertyNamesSeenCount = this.propertyNamesSeenCount.get(propertyNamesHash) || 0;
        this.propertyNamesSeenCount.set(propertyNamesHash, propertyNamesSeenCount + 1);
        let propertyNamesValue = lowSignal;
        if (propertyNamesSeenCount === 0) {
            const propertyNamesKey = this.storage.generateKey();
            this.propertyNamesMap.set(propertyNamesHash, propertyNamesKey);
            this.storage.storeString(propertyNamesKey, JSON.stringify(lowSignal));
            propertyNamesValue = lowSignal;
        }
        else if (propertyNamesSeenCount >= 1) {
            const existingPropertyNamesKey = this.propertyNamesMap.get(propertyNamesHash);
            propertyNamesValue = `[DEDUPLICATED_ARRAY]\nKey: ${existingPropertyNamesKey}`;
        }
        return {
            ...highSignal,
            _compressed_properties_key: compressedKey,
            _compressed_property_names: propertyNamesValue,
            _compression_info: `${lowSignal.length} low-signal properties compressed (URLs, IDs). Use expandTokens with key "${compressedKey}" to retrieve.`
        };
    }
    hashObject(obj) {
        const normalized = JSON.stringify(obj, Object.keys(obj).sort());
        let hash = 0;
        for (let i = 0; i < normalized.length; i++) {
            const char = normalized.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash = hash & hash;
        }
        return hash.toString(36);
    }
    compressJsonProperties(obj, path = "") {
        if (path === "" &&
            this.storage.estimateTokens(JSON.stringify(obj)) <= this.maxTokens) {
            return obj;
        }
        if (Array.isArray(obj)) {
            const processedItems = obj.map((item, index) => this.compressJsonProperties(item, `${path}[${index}]`));
            const finalArray = [];
            let currentChunk = [];
            for (let i = processedItems.length - 1; i >= 0; i--) {
                const item = processedItems[i];
                currentChunk.unshift(item);
                const chunkString = JSON.stringify(currentChunk);
                const chunkTokens = this.storage.estimateTokens(chunkString);
                if (chunkTokens > this.compressionThreshold) {
                    const key = this.storage.generateKey();
                    this.storage.storeString(key, chunkString);
                    const stub = `[COMPRESSED_JSON_ARRAY_CHUNK - ${chunkTokens} tokens, ${currentChunk.length} items]\nKey: ${key}\nPath: ${path}[${i}...${i + currentChunk.length - 1}]\nPreview: ${chunkString.substring(0, 100)}...\n[Use ${this.toolName} tool with key "${key}" to retrieve this chunk]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
                    finalArray.unshift(stub);
                    currentChunk = [];
                }
            }
            if (currentChunk.length > 0) {
                finalArray.unshift(...currentChunk);
            }
            return finalArray;
        }
        if (obj && typeof obj === "object") {
            const objHash = this.hashObject(obj);
            const existingKey = this.deduplicationMap.get(objHash);
            if (existingKey) {
                return `[DEDUPLICATED_OBJECT]\nKey: ${existingKey}\nPath: ${path}\n[Use ${this.toolName} tool with key "${existingKey}" to retrieve content]`;
            }
            const seenCount = this.objectSeenCount.get(objHash) || 0;
            this.objectSeenCount.set(objHash, seenCount + 1);
            const isFirstOccurrence = seenCount === 0;
            const objWithLowSignalCompressed = this.compressObjectWithLowSignalDetection(obj, path);
            const objToProcess = objWithLowSignalCompressed !== obj ? objWithLowSignalCompressed : obj;
            const result = {};
            for (const [key, value] of Object.entries(objToProcess)) {
                const newPath = path ? `${path}.${key}` : key;
                result[key] = this.compressJsonProperties(value, newPath);
            }
            const objectAsString = JSON.stringify(result);
            const tokens = this.storage.estimateTokens(objectAsString);
            if (isFirstOccurrence && tokens > 100) {
                const key = this.storage.generateKey();
                this.deduplicationMap.set(objHash, key);
                this.storage.storeString(key, objectAsString);
                return result;
            }
            if (tokens > this.compressionThreshold) {
                const key = this.storage.generateKey();
                this.storage.storeString(key, objectAsString);
                return `[COMPRESSED_JSON_OBJECT - ${tokens} tokens]\nKey: ${key}\nPath: ${path}\nKeys: ${Object.keys(result).join(", ")}\nPreview: ${objectAsString.substring(0, 200)}...\n[Use ${this.toolName} tool with key "${key}" to retrieve full content]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
            }
            return result;
        }
        if (typeof obj === "string") {
            const parsedJson = this.tryParseJson(obj);
            if (parsedJson) {
                const compressedJson = this.compressJsonProperties(parsedJson, path);
                const compressedJsonString = JSON.stringify(compressedJson, null, 2);
                const originalTokens = this.storage.estimateTokens(obj);
                const compressedTokens = this.storage.estimateTokens(compressedJsonString);
                if (compressedTokens < originalTokens * 0.8) {
                    return compressedJsonString;
                }
            }
            const tokens = this.storage.estimateTokens(obj);
            if (tokens > this.compressionThreshold) {
                const key = this.storage.generateKey();
                this.storage.storeString(key, obj);
                return `[COMPRESSED_JSON_PROPERTY - ${tokens} tokens]\nKey: ${key}\nPath: ${path}\nPreview: ${obj.substring(0, 200)}...\n[Use ${this.toolName} tool with key "${key}" to retrieve full content]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated ${this.toolName} calls - especially useful for JSON data]`;
            }
            return obj;
        }
        return obj;
    }
}
exports.JsonCompressor = JsonCompressor;
//# sourceMappingURL=JsonCompressor.js.map