/**
 * Salesforce REST API Client
 */

import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
  SObject,
  QueryResult,
  SObjectDescribe,
  BulkJob,
  BulkJobInfo,
  CompositeRequest,
  CompositeResponse,
  SalesforceErrorResponse,
  ApiLimits,
} from '../types/index.js';

export interface SalesforceClientConfig {
  accessToken: string;
  instanceUrl: string;
  apiVersion?: string;
}

export class SalesforceClient {
  private client: AxiosInstance;
  private instanceUrl: string;
  private apiVersion: string;
  private describeCache: Map<string, SObjectDescribe> = new Map();
  private apiLimits: ApiLimits | null = null;

  constructor(config: SalesforceClientConfig) {
    this.instanceUrl = config.instanceUrl.replace(/\/$/, '');
    this.apiVersion = config.apiVersion || 'v59.0';

    this.client = axios.create({
      baseURL: `${this.instanceUrl}/services/data/${this.apiVersion}`,
      headers: {
        'Authorization': `Bearer ${config.accessToken}`,
        'Content-Type': 'application/json',
      },
      timeout: 30000,
    });

    // Intercept responses to extract API limit info
    this.client.interceptors.response.use(
      (response) => {
        const limitInfo = response.headers['sforce-limit-info'];
        if (limitInfo) {
          this.parseApiLimits(limitInfo);
        }
        return response;
      },
      (error) => error
    );
  }

  private parseApiLimits(limitInfo: string): void {
    // Format: "api-usage=123/15000"
    const match = limitInfo.match(/api-usage=(\d+)\/(\d+)/);
    if (match) {
      this.apiLimits = {
        used: parseInt(match[1], 10),
        remaining: parseInt(match[2], 10) - parseInt(match[1], 10),
      };
    }
  }

  public getApiLimits(): ApiLimits | null {
    return this.apiLimits;
  }

  /**
   * Retry wrapper with exponential backoff
   */
  private async retryRequest<T>(
    fn: () => Promise<T>,
    retries = 3,
    delay = 1000
  ): Promise<T> {
    try {
      return await fn();
    } catch (error) {
      if (retries === 0) {
        throw error;
      }

      const axiosError = error as AxiosError;
      const shouldRetry =
        axiosError.response?.status === 429 || // Rate limit
        axiosError.response?.status === 503 || // Service unavailable
        axiosError.code === 'ECONNRESET' ||
        axiosError.code === 'ETIMEDOUT';

      if (!shouldRetry) {
        throw error;
      }

      await new Promise((resolve) => setTimeout(resolve, delay));
      return this.retryRequest(fn, retries - 1, delay * 2);
    }
  }

  /**
   * Execute SOQL query
   */
  public async query<T extends SObject = SObject>(
    soql: string
  ): Promise<QueryResult<T>> {
    return this.retryRequest(async () => {
      const response = await this.client.get<QueryResult<T>>('/query', {
        params: { q: soql },
      });
      return response.data;
    });
  }

  /**
   * Query more records using nextRecordsUrl
   */
  public async queryMore<T extends SObject = SObject>(
    nextRecordsUrl: string
  ): Promise<QueryResult<T>> {
    return this.retryRequest(async () => {
      const response = await this.client.get<QueryResult<T>>(nextRecordsUrl);
      return response.data;
    });
  }

  /**
   * Execute SOSL search
   */
  public async search<T extends SObject = SObject>(
    sosl: string
  ): Promise<T[]> {
    return this.retryRequest(async () => {
      const response = await this.client.get<{ searchRecords: T[] }>('/search', {
        params: { q: sosl },
      });
      return response.data.searchRecords;
    });
  }

  /**
   * Get a record by ID
   */
  public async getRecord<T extends SObject = SObject>(
    objectType: string,
    id: string,
    fields?: string[]
  ): Promise<T> {
    return this.retryRequest(async () => {
      const params = fields ? { fields: fields.join(',') } : undefined;
      const response = await this.client.get<T>(`/sobjects/${objectType}/${id}`, {
        params,
      });
      return response.data;
    });
  }

  /**
   * Create a record
   */
  public async createRecord<T extends SObject = SObject>(
    objectType: string,
    data: Partial<T>
  ): Promise<{ id: string; success: boolean; errors: SalesforceErrorResponse[] }> {
    return this.retryRequest(async () => {
      const response = await this.client.post(`/sobjects/${objectType}`, data);
      return response.data;
    });
  }

  /**
   * Update a record
   */
  public async updateRecord<T extends SObject = SObject>(
    objectType: string,
    id: string,
    data: Partial<T>
  ): Promise<void> {
    return this.retryRequest(async () => {
      await this.client.patch(`/sobjects/${objectType}/${id}`, data);
    });
  }

  /**
   * Upsert a record (insert or update based on external ID)
   */
  public async upsertRecord<T extends SObject = SObject>(
    objectType: string,
    externalIdField: string,
    externalIdValue: string,
    data: Partial<T>
  ): Promise<{ id: string; success: boolean; created: boolean }> {
    return this.retryRequest(async () => {
      const response = await this.client.patch(
        `/sobjects/${objectType}/${externalIdField}/${externalIdValue}`,
        data
      );
      return response.data;
    });
  }

  /**
   * Delete a record
   */
  public async deleteRecord(objectType: string, id: string): Promise<void> {
    return this.retryRequest(async () => {
      await this.client.delete(`/sobjects/${objectType}/${id}`);
    });
  }

  /**
   * Describe an SObject (with caching)
   */
  public async describe(objectType: string, skipCache = false): Promise<SObjectDescribe> {
    if (!skipCache && this.describeCache.has(objectType)) {
      return this.describeCache.get(objectType)!;
    }

    return this.retryRequest(async () => {
      const response = await this.client.get<SObjectDescribe>(
        `/sobjects/${objectType}/describe`
      );
      this.describeCache.set(objectType, response.data);
      return response.data;
    });
  }

  /**
   * Execute composite request (up to 25 subrequests)
   */
  public async composite(request: CompositeRequest): Promise<CompositeResponse> {
    return this.retryRequest(async () => {
      const response = await this.client.post<CompositeResponse>(
        '/composite',
        request
      );
      return response.data;
    });
  }

  /**
   * Execute batch request
   */
  public async batch(
    requests: Array<{
      method: string;
      url: string;
      richInput?: Record<string, unknown>;
    }>
  ): Promise<{ hasErrors: boolean; results: unknown[] }> {
    return this.retryRequest(async () => {
      const response = await this.client.post('/composite/batch', {
        batchRequests: requests,
      });
      return response.data;
    });
  }

  /**
   * Create a Bulk API 2.0 job
   */
  public async createBulkJob(job: BulkJob): Promise<BulkJobInfo> {
    return this.retryRequest(async () => {
      const response = await this.client.post<BulkJobInfo>('/jobs/ingest', {
        object: job.object,
        operation: job.operation,
        externalIdFieldName: job.externalIdFieldName,
        contentType: job.contentType || 'CSV',
        lineEnding: job.lineEnding || 'LF',
      });
      return response.data;
    });
  }

  /**
   * Upload data to a bulk job
   */
  public async uploadBulkData(jobId: string, data: string): Promise<void> {
    return this.retryRequest(async () => {
      await this.client.put(`/jobs/ingest/${jobId}/batches`, data, {
        headers: { 'Content-Type': 'text/csv' },
      });
    });
  }

  /**
   * Close/complete a bulk job
   */
  public async closeBulkJob(jobId: string): Promise<BulkJobInfo> {
    return this.retryRequest(async () => {
      const response = await this.client.patch<BulkJobInfo>(`/jobs/ingest/${jobId}`, {
        state: 'UploadComplete',
      });
      return response.data;
    });
  }

  /**
   * Abort a bulk job
   */
  public async abortBulkJob(jobId: string): Promise<BulkJobInfo> {
    return this.retryRequest(async () => {
      const response = await this.client.patch<BulkJobInfo>(`/jobs/ingest/${jobId}`, {
        state: 'Aborted',
      });
      return response.data;
    });
  }

  /**
   * Get bulk job info
   */
  public async getBulkJobInfo(jobId: string): Promise<BulkJobInfo> {
    return this.retryRequest(async () => {
      const response = await this.client.get<BulkJobInfo>(`/jobs/ingest/${jobId}`);
      return response.data;
    });
  }

  /**
   * Get successful results from a bulk job
   */
  public async getBulkJobSuccessfulResults(jobId: string): Promise<string> {
    return this.retryRequest(async () => {
      const response = await this.client.get<string>(
        `/jobs/ingest/${jobId}/successfulResults`,
        { headers: { Accept: 'text/csv' } }
      );
      return response.data;
    });
  }

  /**
   * Get failed results from a bulk job
   */
  public async getBulkJobFailedResults(jobId: string): Promise<string> {
    return this.retryRequest(async () => {
      const response = await this.client.get<string>(
        `/jobs/ingest/${jobId}/failedResults`,
        { headers: { Accept: 'text/csv' } }
      );
      return response.data;
    });
  }

  /**
   * SOQL query builder helper
   */
  public buildSOQL(options: {
    select: string[];
    from: string;
    where?: string;
    orderBy?: string;
    limit?: number;
    offset?: number;
  }): string {
    let soql = `SELECT ${options.select.join(', ')} FROM ${options.from}`;

    if (options.where) {
      soql += ` WHERE ${options.where}`;
    }

    if (options.orderBy) {
      soql += ` ORDER BY ${options.orderBy}`;
    }

    if (options.limit) {
      soql += ` LIMIT ${options.limit}`;
    }

    if (options.offset) {
      soql += ` OFFSET ${options.offset}`;
    }

    return soql;
  }

  /**
   * Helper: Query all records (handles pagination automatically)
   */
  public async queryAll<T extends SObject = SObject>(
    soql: string
  ): Promise<T[]> {
    const allRecords: T[] = [];
    let result = await this.query<T>(soql);
    allRecords.push(...result.records);

    while (!result.done && result.nextRecordsUrl) {
      result = await this.queryMore<T>(result.nextRecordsUrl);
      allRecords.push(...result.records);
    }

    return allRecords;
  }

  /**
   * Clear describe cache
   */
  public clearDescribeCache(): void {
    this.describeCache.clear();
  }
}
