/**
 * @fileoverview Base client for interacting with the DeepSource API
 * This module provides a base client class with core functionality.
 */

import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
import { createLogger } from '../utils/logging/logger.js';
import { handleApiError } from '../utils/errors/handlers.js';
import { GraphQLResponse } from '../types/graphql-responses.js';
import { DeepSourceProject } from '../models/projects.js';
import {
  PaginatedResponse,
  PaginationParams,
  MultiPageOptions,
  PageInfo,
} from '../utils/pagination/types.js';
import { fetchMultiplePages, PageFetcher } from '../utils/pagination/manager.js';
import { handlePageSizeAlias, shouldFetchMultiplePages } from '../utils/pagination/helpers.js';
import { asProjectKey } from '../types/branded.js';
import { executeWithRetry, RetryExecutorOptions } from '../utils/retry/retry-executor.js';

/**
 * Configuration options for the DeepSource client
 * @public
 */
export interface DeepSourceClientConfig {
  /** The base URL for the API */
  baseURL?: string;
  /** Request timeout in milliseconds */
  timeout?: number;
  /** Custom headers to include in requests */
  headers?: Record<string, string>;
}

/**
 * Default configuration for the DeepSource client
 * @private
 */
const DEFAULT_CONFIG: DeepSourceClientConfig = {
  baseURL: 'https://api.deepsource.io/graphql/',
  timeout: 30000,
};

/**
 * Base client for DeepSource API with core HTTP functionality
 * @class
 * @public
 */
export class BaseDeepSourceClient {
  /**
   * HTTP client for making API requests to DeepSource
   * @protected
   */
  protected client: AxiosInstance;

  /**
   * Logger instance for the client
   * @protected
   */
  protected logger = createLogger('DeepSourceClient');

  /**
   * Creates a new BaseDeepSourceClient instance
   * @param apiKey - The DeepSource API key for authentication
   * @param config - Optional configuration options
   * @throws {Error} When apiKey is not provided
   * @public
   */
  constructor(apiKey: string, config: DeepSourceClientConfig = {}) {
    if (!apiKey) {
      throw new Error('DeepSource API key is required');
    }

    // Merge default config with provided config
    const mergedConfig: AxiosRequestConfig = {
      ...DEFAULT_CONFIG,
      ...config,
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        Authorization: `Bearer ${apiKey}`,
        ...config.headers,
      },
    };

    this.client = axios.create(mergedConfig);
  }

  /**
   * Execute a GraphQL query with variables against the DeepSource API
   * @param query The GraphQL query to execute
   * @param variables The variables for the query
   * @returns The query response data
   * @throws {ClassifiedError} When the query fails
   * @protected
   */
  protected async executeGraphQL<T>(
    query: string,
    variables?: Record<string, unknown>
  ): Promise<GraphQLResponse<T>> {
    try {
      // Log full query for debugging
      this.logger.debug('Executing GraphQL query', {
        query,
        variables,
        queryLength: query.length,
        requestHeaders: this.client.defaults.headers,
      });

      // Execute the query
      const startTime = Date.now();
      const response = await this.client.post('', { query, variables });
      const duration = Date.now() - startTime;

      this.logger.debug('GraphQL response received', {
        status: response.status,
        statusText: response.statusText,
        duration: `${duration}ms`,
        dataSize: JSON.stringify(response.data).length,
        hasData: Boolean(response.data?.data),
        hasErrors: Boolean(response.data?.errors),
      });

      // Check for GraphQL errors in the response
      if (response.data.errors) {
        this.logger.error('GraphQL query returned errors', {
          errors: response.data.errors,
          query: query.substring(0, 100) + (query.length > 100 ? '...' : ''),
        });
        throw new Error(`GraphQL Errors: ${JSON.stringify(response.data.errors)}`);
      }

      return response.data as GraphQLResponse<T>;
    } catch (error) {
      // Log detailed error information
      this.logger.error('Error executing GraphQL query', {
        errorType: typeof error,
        errorName: error instanceof Error ? error.name : 'Unknown error type',
        errorMessage: error instanceof Error ? error.message : String(error),
        errorResponse: (error as Record<string, unknown>)?.response
          ? {
              status: (error as Record<string, Record<string, unknown>>).response?.status,
              statusText: (error as Record<string, Record<string, unknown>>).response?.statusText,
              data: (error as Record<string, Record<string, unknown>>).response?.data,
            }
          : 'No response data available',
        query: query.substring(0, 100) + (query.length > 100 ? '...' : ''),
      });

      // Handle the error properly
      const handledError = handleApiError(error);
      this.logger.debug('Handled API error', {
        originalMessage: error instanceof Error ? error.message : String(error),
        handledMessage: handledError.message,
        handledName: handledError.name,
      });

      throw handledError;
    }
  }

  /**
   * Execute a GraphQL query with automatic retry logic
   * @param query The GraphQL query to execute
   * @param variables The variables for the query
   * @param endpoint The endpoint name for retry policy selection
   * @returns The query response data
   * @throws {ClassifiedError} When the query fails after all retries
   * @protected
   */
  protected async executeGraphQLWithRetry<T>(
    query: string,
    variables?: Record<string, unknown>,
    endpoint = 'default'
  ): Promise<GraphQLResponse<T>> {
    // Determine if this is a read operation (queries are always safe to retry)
    const isQuery = query.trim().toLowerCase().startsWith('query');

    // Only apply retry logic to queries, not mutations
    if (!isQuery) {
      this.logger.debug('Skipping retry for mutation', { endpoint });
      return this.executeGraphQL<T>(query, variables);
    }

    const retryOptions: RetryExecutorOptions = {
      endpoint,
      extractRetryAfter: (error: unknown) => {
        // Extract Retry-After header from Axios errors
        if (BaseDeepSourceClient.isAxiosError(error) && error.response?.headers?.['retry-after']) {
          return error.response.headers['retry-after'];
        }
        return undefined;
      },
      onRetryAttempt: (context) => {
        this.logger.info('Retrying GraphQL query', {
          endpoint: context.endpoint,
          attempt: context.attemptNumber + 1,
          elapsed: context.elapsedMs,
          isLastAttempt: context.isLastAttempt,
        });
      },
    };

    const result = await executeWithRetry(
      () => this.executeGraphQL<T>(query, variables),
      retryOptions
    );

    if (result.success && result.data) {
      return result.data;
    }

    // If we got here, all retries failed
    if (result.circuitBreakerBlocked) {
      const error = new Error(`Circuit breaker is open for endpoint: ${endpoint}`);
      throw handleApiError(error);
    }

    if (result.budgetExhausted) {
      const error = new Error(`Retry budget exhausted for endpoint: ${endpoint}`);
      throw handleApiError(error);
    }

    // Re-throw the last error
    throw result.error || new Error('Unknown error after retries');
  }

  /**
   * Type guard to check if an error is an Axios error
   * @param error The error to check
   * @returns True if it's an Axios error
   * @private
   */
  private static isAxiosError(error: unknown): error is AxiosError {
    return (
      error !== null &&
      typeof error === 'object' &&
      'isAxiosError' in error &&
      (error as AxiosError).isAxiosError === true
    );
  }

  /**
   * Execute a GraphQL mutation against the DeepSource API
   * @param mutation The GraphQL mutation to execute
   * @param variables The variables for the mutation
   * @returns The mutation response data
   * @throws {ClassifiedError} When the mutation fails
   * @protected
   */
  protected async executeGraphQLMutation<T>(
    mutation: string,
    variables?: Record<string, unknown>
  ): Promise<T> {
    try {
      this.logger.debug('Executing GraphQL mutation', { mutation, variables });
      const response = await this.client.post('', { query: mutation, variables });

      // Check for GraphQL errors in the response
      if (response.data.errors) {
        this.logger.error('GraphQL mutation returned errors', response.data.errors);
        throw new Error(`GraphQL Errors: ${JSON.stringify(response.data.errors)}`);
      }

      return response.data as T;
    } catch (error) {
      this.logger.error('Error executing GraphQL mutation', error);
      throw handleApiError(error);
    }
  }

  /**
   * Finds a project by its key using a simplified projects query
   * @param projectKey The project key to find
   * @returns The project if found, null otherwise
   * @protected
   */
  protected async findProjectByKey(projectKey: string): Promise<DeepSourceProject | null> {
    try {
      const brandedKey = asProjectKey(projectKey);
      // Simple cache or fetch implementation
      // For now, we'll use a simplified approach and assume the project exists
      // In a real implementation, this could cache results or use a dedicated query
      return {
        key: brandedKey,
        name: 'Project', // Simplified
        repository: {
          url: projectKey,
          provider: 'github', // Default assumption
          login: projectKey.split('/')[0] || 'unknown',
          name: projectKey.split('/')[1] || 'unknown',
          isPrivate: false,
          isActivated: true,
        },
      };
    } catch (error) {
      this.logger.error('Error finding project by key', { projectKey, error });
      return null;
    }
  }

  /**
   * Normalizes pagination parameters to ensure they're valid
   * @param params The pagination parameters to normalize
   * @returns Normalized pagination parameters
   * @protected
   */
  protected static normalizePaginationParams(params: PaginationParams): PaginationParams {
    const normalizedParams: PaginationParams = {};

    // Ensure offset is a non-negative integer or undefined
    if (params.offset !== undefined) {
      normalizedParams.offset = Math.max(0, Math.floor(Number(params.offset)));
    }

    // Ensure first is a positive integer or undefined
    if (params.first !== undefined) {
      normalizedParams.first = Math.max(1, Math.floor(Number(params.first)));
    }

    // Ensure last is a positive integer or undefined
    if (params.last !== undefined) {
      normalizedParams.last = Math.max(1, Math.floor(Number(params.last)));
    }

    // Ensure after and before are strings if provided
    if (params.after !== undefined) {
      if (typeof params.after === 'string') {
        normalizedParams.after = params.after;
      } else {
        normalizedParams.after = String(params.after);
      }
    }

    if (params.before !== undefined) {
      if (typeof params.before === 'string') {
        normalizedParams.before = params.before;
      } else {
        normalizedParams.before = String(params.before);
      }
    }

    // Handle cursor-based pagination precedence
    if (normalizedParams.before) {
      // When fetching backwards with 'before', prioritize 'last'
      const lastValue = normalizedParams.last ?? normalizedParams.first ?? 10;
      normalizedParams.last = lastValue;
      // Clear 'first' and 'after' to avoid conflicts
      delete normalizedParams.first;
      delete normalizedParams.after;
    } else if (normalizedParams.after) {
      // When fetching forwards with 'after', prioritize 'first'
      const firstValue = normalizedParams.first ?? 10;
      normalizedParams.first = firstValue;
      // Clear 'last' and 'before' to avoid conflicts
      delete normalizedParams.last;
      delete normalizedParams.before;
    }

    return normalizedParams;
  }

  /**
   * Creates an empty paginated response
   * @returns An empty paginated response
   * @protected
   */
  protected static createEmptyPaginatedResponse<T>(): PaginatedResponse<T> {
    return {
      items: [],
      pageInfo: {
        hasNextPage: false,
        hasPreviousPage: false,
      },
      totalCount: 0,
    };
  }

  /**
   * Extracts error messages from GraphQL errors array
   * @param errors Array of GraphQL errors
   * @returns Combined error message string
   * @protected
   */
  protected static extractErrorMessages(errors: Array<{ message: string }>): string {
    return errors.map((error) => error.message).join('; ');
  }

  /**
   * Fetches data with automatic pagination support
   * Handles multi-page fetching when max_pages is specified
   * @template T The type of items being fetched
   * @param fetcher Single page fetcher function
   * @param params Pagination parameters including potential max_pages
   * @returns Paginated response with all fetched items
   * @protected
   */
  protected async fetchWithPagination<T>(
    fetcher: (params: PaginationParams) => Promise<PaginatedResponse<T>>,
    params: PaginationParams
  ): Promise<PaginatedResponse<T>> {
    // Handle page_size alias
    const processedParams = handlePageSizeAlias(params);

    // Check if multi-page fetching is needed
    if (shouldFetchMultiplePages(processedParams) && processedParams.max_pages !== undefined) {
      const maxPages = processedParams.max_pages;

      // Create a page fetcher wrapper for the pagination manager
      const pageFetcher: PageFetcher<T> = async (cursor, pageSize) => {
        const pageParams: PaginationParams = {
          ...processedParams,
          first: pageSize || processedParams.first || 50,
        };
        if (cursor) {
          pageParams.after = cursor;
        }
        delete pageParams.max_pages; // Remove max_pages from individual requests
        return fetcher(pageParams);
      };

      // Fetch multiple pages
      const options: MultiPageOptions = {
        maxPages,
        pageSize: processedParams.first || processedParams.page_size || 50,
        onProgress: (pagesFetched, itemsFetched) => {
          this.logger.debug('Pagination progress', { pagesFetched, itemsFetched });
        },
      };

      const result = await fetchMultiplePages(pageFetcher, options);

      // Create a merged response
      const pageInfo: PageInfo = {
        hasNextPage: result.hasMore,
        hasPreviousPage: false, // First page for multi-page fetch
      };
      if (result.lastCursor) {
        pageInfo.endCursor = result.lastCursor;
      }

      return {
        items: result.items,
        pageInfo,
        totalCount: result.totalCount || result.items.length,
      };
    }

    // Single page fetch
    return fetcher(processedParams);
  }
}
