/**
 * 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 type { QueryRequest, QueryResponse, Group, ResourceMetadata, TagValueParam } from './types.js';
import { httpFetch } from '../utils/http.js';

const maxErrorBodyLength = 2048;
const maxToolResultLength = 32 * 1024;

function truncateText(text: string, maxLength: number): string {
  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 {
  private baseUrl: string;

  constructor(address: string) {
    this.baseUrl = BanyanDBClient.buildBaseUrl(address);
  }

  private static buildBaseUrl(address: string): string {
    const trimmedAddress = address.trim();
    const addressWithProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmedAddress)
      ? trimmedAddress
      : `http://${trimmedAddress}`;

    let parsedAddress: URL;
    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: string, params?: TagValueParam[]): Promise<string> {
    const request: QueryRequest = {
      query: bydbqlQuery,
    };
    if (params && params.length > 0) {
      request.params = params;
    }

    const url = `${this.baseUrl}/v1/bydbql/query`;

    const queryDebugInfo = `Query: "${bydbqlQuery}"\nURL: ${url}`;

    try {
      const data = (await httpFetch({
        url,
        method: 'POST',
        json: request,
      })) as QueryResponse | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).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.
   */
  private formatResponse(response: QueryResponse): string {
    // 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(): Promise<Group[]> {
    const url = `${this.baseUrl}/v1/group/schema/lists`;

    try {
      const data = (await httpFetch({
        url,
        method: 'GET',
        json: null,
      })) as { group?: Group[] } | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).errors;
        const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
        throw new Error(`Failed to list groups: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
      }

      return (data as { group?: Group[] }).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: string): Promise<ResourceMetadata[]> {
    const url = `${this.baseUrl}/v1/stream/schema/lists/${encodeURIComponent(group)}`;

    try {
      const data = (await httpFetch({
        url,
        method: 'GET',
        json: null,
      })) as { stream?: ResourceMetadata[] } | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).errors;
        const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
        throw new Error(`Failed to list streams: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
      }

      return (data as { stream?: ResourceMetadata[] }).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: string): Promise<ResourceMetadata[]> {
    const url = `${this.baseUrl}/v1/measure/schema/lists/${encodeURIComponent(group)}`;

    try {
      const data = (await httpFetch({
        url,
        method: 'GET',
        json: null,
      })) as { measure?: ResourceMetadata[] } | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).errors;
        const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
        throw new Error(`Failed to list measures: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
      }

      return (data as { measure?: ResourceMetadata[] }).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: string): Promise<ResourceMetadata[]> {
    const url = `${this.baseUrl}/v1/trace/schema/lists/${encodeURIComponent(group)}`;

    try {
      const data = (await httpFetch({
        url,
        method: 'GET',
        json: null,
      })) as { trace?: ResourceMetadata[] } | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).errors;
        const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
        throw new Error(`Failed to list traces: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`);
      }

      return (data as { trace?: ResourceMetadata[] }).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: string): Promise<ResourceMetadata[]> {
    const url = `${this.baseUrl}/v1/property/schema/lists/${encodeURIComponent(group)}`;

    try {
      const data = (await httpFetch({
        url,
        method: 'GET',
        json: null,
      })) as { properties?: ResourceMetadata[] } | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).errors;
        const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
        throw new Error(
          `Failed to list properties: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`,
        );
      }

      return (data as { properties?: ResourceMetadata[] }).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: string): Promise<ResourceMetadata[]> {
    const url = `${this.baseUrl}/v1/topn-agg/schema/lists/${encodeURIComponent(group)}`;

    try {
      const data = (await httpFetch({
        url,
        method: 'GET',
        json: null,
      })) as { topNAggregation?: ResourceMetadata[] } | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).errors;
        const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
        throw new Error(
          `Failed to list topN aggregations: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`,
        );
      }

      return (data as { topNAggregation?: ResourceMetadata[] }).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: string): Promise<ResourceMetadata[]> {
    const url = `${this.baseUrl}/v1/index-rule/schema/lists/${encodeURIComponent(group)}`;

    try {
      const data = (await httpFetch({
        url,
        method: 'GET',
        json: null,
      })) as { indexRule?: ResourceMetadata[] } | { errors: Response };

      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).errors;
        const errorText = await errorResponse.text().catch(() => errorResponse.statusText);
        throw new Error(
          `Failed to list index rules: ${errorResponse.status} ${errorResponse.statusText} - ${errorText}`,
        );
      }

      return (data as { indexRule?: ResourceMetadata[] }).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: string): Promise<string> {
    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 },
      })) as unknown | { errors: Response };

      // Check if httpFetch returned an error
      if (data && typeof data === 'object' && 'errors' in data) {
        const errorResponse = (data as { errors: Response }).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)}`);
    }
  }
}

// Re-export types for convenience
export type { ResourceMetadata } from './types.js';
