/**
 * Licensed to Apache Software Foundation (ASF) under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Apache Software Foundation (ASF) licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { httpFetch } from '../utils/http.js';
const maxErrorBodyLength = 2048;
const maxToolResultLength = 32 * 1024;
function truncateText(text, maxLength) {
    if (text.length <= maxLength) {
        return text;
    }
    return `${text.slice(0, maxLength)}\n... [truncated ${text.length - maxLength} characters]`;
}
/**
 * BanyanDBClient wraps the BanyanDB HTTP client for executing queries.
 * Uses the HTTP API (grpc-gateway) instead of gRPC to avoid proto dependency issues.
 */
export class BanyanDBClient {
    baseUrl;
    constructor(address) {
        this.baseUrl = BanyanDBClient.buildBaseUrl(address);
    }
    static buildBaseUrl(address) {
        const trimmedAddress = address.trim();
        const addressWithProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmedAddress)
            ? trimmedAddress
            : `http://${trimmedAddress}`;
        let parsedAddress;
        try {
            parsedAddress = new URL(addressWithProtocol);
        }
        catch (error) {
            throw new Error(`Invalid BANYANDB_ADDRESS "${address}": ${error instanceof Error ? error.message : String(error)}`);
        }
        const protocol = parsedAddress.protocol === 'https:' ? 'https:' : 'http:';
        const hostname = parsedAddress.hostname.includes(':') ? `[${parsedAddress.hostname}]` : parsedAddress.hostname;
        const port = !parsedAddress.port || parsedAddress.port === '17900' ? '17913' : parsedAddress.port;
        const pathname = parsedAddress.pathname === '/' ? '' : parsedAddress.pathname.replace(/\/$/, '');
        const apiPath = pathname.endsWith('/api') ? pathname : `${pathname}/api`;
        return `${protocol}//${hostname}:${port}${apiPath}`;
    }
    /**
     * Execute a BydbQL query and return the result as a formatted string.
     */
    async query(bydbqlQuery) {
        const request = {
            query: bydbqlQuery,
        };
        const url = `${this.baseUrl}/v1/bydbql/query`;
        const queryDebugInfo = `Query: "${bydbqlQuery}"\nURL: ${url}`;
        try {
            const data = (await httpFetch({
                url,
                method: 'POST',
                json: request,
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Query execution failed: ${errorResponse.status} ${errorResponse.statusText}\n\n${queryDebugInfo}\n\nResponse: ${truncateText(errorText, maxErrorBodyLength)}`);
            }
            // Check if response has content
            if (!data) {
                throw new Error(`Empty response body from BanyanDB\n\n${queryDebugInfo}`);
            }
            // Check for result types both at top level and inside result wrapper
            const hasResultType = data.streamResult ||
                data.measureResult ||
                data.traceResult ||
                data.propertyResult ||
                data.topnResult ||
                (data.result &&
                    (data.result.streamResult ||
                        data.result.measureResult ||
                        data.result.traceResult ||
                        data.result.propertyResult ||
                        data.result.topnResult));
            if (!hasResultType) {
                // This is a valid response but with no result type - likely an empty result set
                const debugMsg = `Response has no result type fields\n\n${queryDebugInfo}\n\nFull response: ${JSON.stringify(data, null, 2)}`;
                return (`Query executed successfully but returned no results.\n\n${debugMsg}\n\nPossible reasons:\n` +
                    '- The query matched no data (empty result set)\n' +
                    '- The resource name or group name might be incorrect\n' +
                    '- The time range might not contain any data\n\n' +
                    'Try:\n' +
                    '1. Use list_groups_schemas to verify the resource exists\n' +
                    '2. Check the time range in your query\n' +
                    '3. Verify the resource name and group name are correct');
            }
            // Format the response based on the result type
            return this.formatResponse(data);
        }
        catch (error) {
            if (error instanceof Error) {
                // Check if it's a timeout error
                if (error.name === 'AbortError' || error.message.includes('aborted') || error.message.includes('timeout')) {
                    throw new Error(`Query timeout. BanyanDB may be slow or unresponsive. ` +
                        `Check that BanyanDB is running and accessible at ${this.baseUrl}\n\n${queryDebugInfo}`);
                }
                // Check if it's a network/connection error
                if (error.message.includes('fetch failed') ||
                    error.message.includes('ECONNREFUSED') ||
                    error.message.includes('Failed to fetch') ||
                    error.name === 'TypeError') {
                    throw new Error(`Failed to connect to BanyanDB at ${this.baseUrl}. ` +
                        `Please ensure BanyanDB is running and accessible. ` +
                        `Check that the HTTP API is enabled on port 17913 (or the port you specified). ` +
                        `You can verify by running: curl http://localhost:17913/api/v1/bydbql/query\n\n${queryDebugInfo}`);
                }
                // If error already contains queryDebugInfo, don't duplicate it
                if (!error.message.includes(queryDebugInfo)) {
                    throw new Error(`${error.message}\n\n${queryDebugInfo}`);
                }
                throw error;
            }
            throw new Error(`Query execution failed: ${String(error)}\n\n${queryDebugInfo}`);
        }
    }
    /**
     * Format the query response into a readable string.
     */
    formatResponse(response) {
        // Handle both wrapped and direct response structures
        const streamResult = response.streamResult || response.result?.streamResult;
        const measureResult = response.measureResult || response.result?.measureResult;
        const traceResult = response.traceResult || response.result?.traceResult;
        const propertyResult = response.propertyResult || response.result?.propertyResult;
        const topnResult = response.topnResult || response.result?.topnResult;
        if (streamResult) {
            // Check if it's an empty result set
            if (streamResult.elements && Array.isArray(streamResult.elements) && streamResult.elements.length === 0) {
                return 'Stream Query Result: No data found (empty result set)';
            }
            return truncateText(`Stream Query Result:\n${JSON.stringify(streamResult, null, 2)}`, maxToolResultLength);
        }
        if (measureResult) {
            const dataPoints = measureResult.dataPoints ?? measureResult.data_points;
            if (Array.isArray(dataPoints) && dataPoints.length === 0) {
                return ('Measure Query Result: No data found (empty result set)\n\n' +
                    'Possible reasons:\n' +
                    '- No data exists for the specified time range\n' +
                    '- The measure name or group name might be incorrect\n' +
                    '- The time range might not contain any data points\n\n' +
                    'Suggestions:\n' +
                    '1. Use list_groups_schemas to verify the measure exists:\n' +
                    '   - List groups: list_groups_schemas with resource_type="groups"\n' +
                    '   - List measures: list_groups_schemas with resource_type="measures" and group="<group_name>"\n' +
                    "2. Try expanding the time range (e.g., TIME >= '-24h' for last 24 hours)\n" +
                    '3. Verify data was written to BanyanDB for this measure');
            }
            return truncateText(`Measure Query Result:\n${JSON.stringify(dataPoints, null, 2)}`, maxToolResultLength);
        }
        if (traceResult) {
            // Check if it's an empty result set
            if (traceResult.elements && Array.isArray(traceResult.elements) && traceResult.elements.length === 0) {
                return 'Trace Query Result: No data found (empty result set)';
            }
            return truncateText(`Trace Query Result:\n${JSON.stringify(traceResult, null, 2)}`, maxToolResultLength);
        }
        if (propertyResult) {
            // Check if it's an empty result set
            if (propertyResult.items && Array.isArray(propertyResult.items) && propertyResult.items.length === 0) {
                return 'Property Query Result: No data found (empty result set)';
            }
            return truncateText(`Property Query Result:\n${JSON.stringify(propertyResult, null, 2)}`, maxToolResultLength);
        }
        if (topnResult) {
            // Check if it's an empty result set
            if (topnResult.lists && Array.isArray(topnResult.lists) && topnResult.lists.length === 0) {
                return 'TopN Query Result: No data found (empty result set)';
            }
            return truncateText(`TopN Query Result:\n${JSON.stringify(topnResult, null, 2)}`, maxToolResultLength);
        }
        return 'Unknown result type';
    }
    /**
     * List all groups.
     */
    async listGroups() {
        const url = `${this.baseUrl}/v1/group/schema/lists`;
        try {
            const data = (await httpFetch({
                url,
                method: 'GET',
                json: null,
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to list groups: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return data.group || [];
        }
        catch (error) {
            if (error instanceof Error) {
                if (error.name === 'AbortError' || error.message.includes('aborted') || error.message.includes('timeout')) {
                    throw new Error(`List groups timeout. BanyanDB may be slow or unresponsive. ` +
                        `Check that BanyanDB is running and accessible at ${this.baseUrl}`);
                }
                if (error.message.includes('fetch failed') ||
                    error.message.includes('ECONNREFUSED') ||
                    error.message.includes('Failed to fetch') ||
                    error.name === 'TypeError') {
                    throw new Error(`Failed to connect to BanyanDB at ${this.baseUrl}. ` + `Please ensure BanyanDB is running and accessible.`);
                }
                throw error;
            }
            throw new Error(`Failed to list groups: ${String(error)}`);
        }
    }
    /**
     * List streams in a group.
     */
    async listStreams(group) {
        const url = `${this.baseUrl}/v1/stream/schema/lists/${encodeURIComponent(group)}`;
        try {
            const data = (await httpFetch({
                url,
                method: 'GET',
                json: null,
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to list streams: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return data.stream || [];
        }
        catch (error) {
            if (error instanceof Error) {
                throw error;
            }
            throw new Error(`Failed to list streams: ${String(error)}`);
        }
    }
    /**
     * List measures in a group.
     */
    async listMeasures(group) {
        const url = `${this.baseUrl}/v1/measure/schema/lists/${encodeURIComponent(group)}`;
        try {
            const data = (await httpFetch({
                url,
                method: 'GET',
                json: null,
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to list measures: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return data.measure || [];
        }
        catch (error) {
            if (error instanceof Error) {
                throw error;
            }
            throw new Error(`Failed to list measures: ${String(error)}`);
        }
    }
    /**
     * List traces in a group.
     */
    async listTraces(group) {
        const url = `${this.baseUrl}/v1/trace/schema/lists/${encodeURIComponent(group)}`;
        try {
            const data = (await httpFetch({
                url,
                method: 'GET',
                json: null,
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to list traces: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return data.trace || [];
        }
        catch (error) {
            if (error instanceof Error) {
                throw error;
            }
            throw new Error(`Failed to list traces: ${String(error)}`);
        }
    }
    /**
     * List properties in a group.
     */
    async listProperties(group) {
        const url = `${this.baseUrl}/v1/property/schema/lists/${encodeURIComponent(group)}`;
        try {
            const data = (await httpFetch({
                url,
                method: 'GET',
                json: null,
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to list properties: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return data.properties || [];
        }
        catch (error) {
            if (error instanceof Error) {
                throw error;
            }
            throw new Error(`Failed to list properties: ${String(error)}`);
        }
    }
    /**
     * List topN aggregations in a group.
     */
    async listTopN(group) {
        const url = `${this.baseUrl}/v1/topn-agg/schema/lists/${encodeURIComponent(group)}`;
        try {
            const data = (await httpFetch({
                url,
                method: 'GET',
                json: null,
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to list topN aggregations: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return data.topNAggregation || [];
        }
        catch (error) {
            if (error instanceof Error) {
                if (error.name === 'AbortError' || error.message.includes('aborted') || error.message.includes('timeout')) {
                    throw new Error(`List topN aggregations timeout. ` +
                        `BanyanDB may be slow or unresponsive. ` +
                        `Check that BanyanDB is running and accessible at ${this.baseUrl}`);
                }
                if (error.message.includes('fetch failed') ||
                    error.message.includes('ECONNREFUSED') ||
                    error.message.includes('Failed to fetch') ||
                    error.name === 'TypeError') {
                    throw new Error(`Failed to connect to BanyanDB at ${this.baseUrl}. ` + `Please ensure BanyanDB is running and accessible.`);
                }
                throw error;
            }
            throw new Error(`Failed to list topN aggregations: ${String(error)}`);
        }
    }
    /**
     * List index rules in a group.
     */
    async listIndexRule(group) {
        const url = `${this.baseUrl}/v1/index-rule/schema/lists/${encodeURIComponent(group)}`;
        try {
            const data = (await httpFetch({
                url,
                method: 'GET',
                json: null,
            }));
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to list index rules: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return data.indexRule || [];
        }
        catch (error) {
            if (error instanceof Error) {
                if (error.name === 'AbortError' || error.message.includes('aborted') || error.message.includes('timeout')) {
                    throw new Error(`List index rules timeout. ` +
                        `BanyanDB may be slow or unresponsive. ` +
                        `Check that BanyanDB is running and accessible at ${this.baseUrl}`);
                }
                if (error.message.includes('fetch failed') ||
                    error.message.includes('ECONNREFUSED') ||
                    error.message.includes('Failed to fetch') ||
                    error.name === 'TypeError') {
                    throw new Error(`Failed to connect to BanyanDB at ${this.baseUrl}. ` + `Please ensure BanyanDB is running and accessible.`);
                }
                throw error;
            }
            throw new Error(`Failed to list index rules: ${String(error)}`);
        }
    }
    /**
     * Create a group using JSON format.
     */
    async createGroup(groupJson) {
        const url = `${this.baseUrl}/v1/group/schema`;
        try {
            // Parse JSON string to object
            const group = JSON.parse(groupJson);
            const data = (await httpFetch({
                url,
                method: 'POST',
                json: { group },
            }));
            // Check if httpFetch returned an error
            if (data && typeof data === 'object' && 'errors' in data) {
                const errorResponse = data.errors;
                const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
                throw new Error(`Failed to create group: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
            }
            return `Group "${group.metadata?.name || 'unknown'}" created successfully`;
        }
        catch (error) {
            if (error instanceof Error) {
                if (error.message.includes('JSON')) {
                    throw new Error(`Invalid JSON format: ${error.message}`);
                }
                throw error;
            }
            throw new Error(`Failed to create group: ${String(error)}`);
        }
    }
}
//# sourceMappingURL=index.js.map