/**
 * HubSpot API Client
 * Handles authentication, pagination, rate limiting, and batch operations
 */

import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import type {
  HubSpotObject,
  PagedResponse,
  SearchRequest,
  SearchResponse,
  BatchReadRequest,
  BatchCreateRequest,
  BatchUpdateRequest,
  BatchArchiveRequest,
  BatchResponse,
  HubSpotError,
  AssociationResult,
  BatchAssociation,
} from '../types/index.js';

export interface HubSpotClientConfig {
  accessToken: string;
  baseURL?: string;
  maxRetries?: number;
  retryDelay?: number;
}

export class HubSpotClient {
  private client: AxiosInstance;
  private maxRetries: number;
  private retryDelay: number;

  constructor(config: HubSpotClientConfig) {
    this.maxRetries = config.maxRetries ?? 3;
    this.retryDelay = config.retryDelay ?? 1000;

    this.client = axios.create({
      baseURL: config.baseURL ?? 'https://api.hubapi.com',
      headers: {
        Authorization: `Bearer ${config.accessToken}`,
        'Content-Type': 'application/json',
      },
      timeout: 30000,
    });

    // Response interceptor for error handling
    this.client.interceptors.response.use(
      (response) => response,
      async (error: AxiosError<HubSpotError>) => {
        if (error.response?.status === 429) {
          // Rate limit hit - implement exponential backoff
          const retryAfter = parseInt(error.response.headers['retry-after'] || '1', 10);
          await this.sleep(retryAfter * 1000);
          return this.client.request(error.config!);
        }
        throw this.handleError(error);
      }
    );
  }

  /**
   * Generic request with retry logic
   */
  private async request<T>(
    config: AxiosRequestConfig,
    retries = 0
  ): Promise<T> {
    try {
      const response = await this.client.request<T>(config);
      return response.data;
    } catch (error) {
      if (retries < this.maxRetries && this.isRetryableError(error)) {
        await this.sleep(this.retryDelay * Math.pow(2, retries));
        return this.request<T>(config, retries + 1);
      }
      throw error;
    }
  }

  /**
   * Check if error is retryable
   */
  private isRetryableError(error: any): boolean {
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      return (
        status === 429 || // Rate limit
        status === 500 || // Server error
        status === 502 || // Bad gateway
        status === 503 || // Service unavailable
        status === 504    // Gateway timeout
      );
    }
    return false;
  }

  /**
   * Handle and format errors
   */
  private handleError(error: AxiosError<HubSpotError>): Error {
    if (error.response?.data) {
      const hubspotError = error.response.data;
      return new Error(
        `HubSpot API Error: ${hubspotError.message} (${hubspotError.status}) [${hubspotError.correlationId}]`
      );
    }
    return new Error(`HubSpot API Error: ${error.message}`);
  }

  /**
   * Sleep utility for retry delays
   */
  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  // ===== CRM Objects API =====

  /**
   * Get a single object by ID
   */
  async getObject<T extends Record<string, any>>(
    objectType: string,
    objectId: string,
    properties?: string[],
    associations?: string[]
  ): Promise<HubSpotObject<T>> {
    const params: Record<string, any> = {};
    if (properties?.length) {
      params.properties = properties.join(',');
    }
    if (associations?.length) {
      params.associations = associations.join(',');
    }

    return this.request<HubSpotObject<T>>({
      method: 'GET',
      url: `/crm/v3/objects/${objectType}/${objectId}`,
      params,
    });
  }

  /**
   * List objects with pagination
   */
  async listObjects<T extends Record<string, any>>(
    objectType: string,
    options?: {
      limit?: number;
      after?: string;
      properties?: string[];
      associations?: string[];
    }
  ): Promise<PagedResponse<HubSpotObject<T>>> {
    const params: Record<string, any> = {
      limit: Math.min(options?.limit ?? 100, 100),
    };

    if (options?.after) {
      params.after = options.after;
    }
    if (options?.properties?.length) {
      params.properties = options.properties.join(',');
    }
    if (options?.associations?.length) {
      params.associations = options.associations.join(',');
    }

    return this.request<PagedResponse<HubSpotObject<T>>>({
      method: 'GET',
      url: `/crm/v3/objects/${objectType}`,
      params,
    });
  }

  /**
   * Create a new object
   */
  async createObject<T extends Record<string, any>>(
    objectType: string,
    properties: T,
    associations?: Array<{ to: { id: string }; types: Array<{ associationCategory: string; associationTypeId: number }> }>
  ): Promise<HubSpotObject<T>> {
    return this.request<HubSpotObject<T>>({
      method: 'POST',
      url: `/crm/v3/objects/${objectType}`,
      data: {
        properties,
        associations,
      },
    });
  }

  /**
   * Update an existing object
   */
  async updateObject<T extends Record<string, any>>(
    objectType: string,
    objectId: string,
    properties: Partial<T>
  ): Promise<HubSpotObject<T>> {
    return this.request<HubSpotObject<T>>({
      method: 'PATCH',
      url: `/crm/v3/objects/${objectType}/${objectId}`,
      data: { properties },
    });
  }

  /**
   * Archive (soft delete) an object
   */
  async archiveObject(objectType: string, objectId: string): Promise<void> {
    await this.request({
      method: 'DELETE',
      url: `/crm/v3/objects/${objectType}/${objectId}`,
    });
  }

  // ===== Search API =====

  /**
   * Search objects with filters and pagination
   */
  async searchObjects<T extends Record<string, any>>(
    objectType: string,
    searchRequest: SearchRequest
  ): Promise<SearchResponse<HubSpotObject<T>>> {
    return this.request<SearchResponse<HubSpotObject<T>>>({
      method: 'POST',
      url: `/crm/v3/objects/${objectType}/search`,
      data: searchRequest,
    });
  }

  /**
   * Search all results with automatic pagination (up to 10,000 limit)
   */
  async searchAllObjects<T extends Record<string, any>>(
    objectType: string,
    searchRequest: SearchRequest,
    maxResults = 10000
  ): Promise<HubSpotObject<T>[]> {
    const allResults: HubSpotObject<T>[] = [];
    let after: string | undefined;
    const limit = Math.min(searchRequest.limit ?? 100, 100);

    do {
      const response = await this.searchObjects<T>(objectType, {
        ...searchRequest,
        limit,
        after,
      });

      allResults.push(...response.results);
      after = response.paging?.next?.after;

      if (allResults.length >= maxResults) {
        break;
      }
    } while (after);

    return allResults.slice(0, maxResults);
  }

  // ===== Batch API =====

  /**
   * Batch read objects
   */
  async batchReadObjects<T extends Record<string, any>>(
    objectType: string,
    request: BatchReadRequest
  ): Promise<BatchResponse<HubSpotObject<T>>> {
    return this.request<BatchResponse<HubSpotObject<T>>>({
      method: 'POST',
      url: `/crm/v3/objects/${objectType}/batch/read`,
      data: request,
    });
  }

  /**
   * Batch create objects
   */
  async batchCreateObjects<T extends Record<string, any>>(
    objectType: string,
    request: BatchCreateRequest<T>
  ): Promise<BatchResponse<HubSpotObject<T>>> {
    return this.request<BatchResponse<HubSpotObject<T>>>({
      method: 'POST',
      url: `/crm/v3/objects/${objectType}/batch/create`,
      data: request,
    });
  }

  /**
   * Batch update objects
   */
  async batchUpdateObjects<T extends Record<string, any>>(
    objectType: string,
    request: BatchUpdateRequest<T>
  ): Promise<BatchResponse<HubSpotObject<T>>> {
    return this.request<BatchResponse<HubSpotObject<T>>>({
      method: 'POST',
      url: `/crm/v3/objects/${objectType}/batch/update`,
      data: request,
    });
  }

  /**
   * Batch archive objects
   */
  async batchArchiveObjects(
    objectType: string,
    request: BatchArchiveRequest
  ): Promise<void> {
    await this.request({
      method: 'POST',
      url: `/crm/v3/objects/${objectType}/batch/archive`,
      data: request,
    });
  }

  // ===== Associations API =====

  /**
   * Get associations for an object
   */
  async getAssociations(
    fromObjectType: string,
    fromObjectId: string,
    toObjectType: string
  ): Promise<AssociationResult> {
    return this.request<AssociationResult>({
      method: 'GET',
      url: `/crm/v4/objects/${fromObjectType}/${fromObjectId}/associations/${toObjectType}`,
    });
  }

  /**
   * Create an association between objects
   */
  async createAssociation(
    fromObjectType: string,
    fromObjectId: string,
    toObjectType: string,
    toObjectId: string,
    associationTypeId: number
  ): Promise<void> {
    await this.request({
      method: 'PUT',
      url: `/crm/v4/objects/${fromObjectType}/${fromObjectId}/associations/${toObjectType}/${toObjectId}`,
      data: [
        {
          associationCategory: 'HUBSPOT_DEFINED',
          associationTypeId,
        },
      ],
    });
  }

  /**
   * Remove an association between objects
   */
  async removeAssociation(
    fromObjectType: string,
    fromObjectId: string,
    toObjectType: string,
    toObjectId: string,
    associationTypeId: number
  ): Promise<void> {
    await this.request({
      method: 'DELETE',
      url: `/crm/v4/objects/${fromObjectType}/${fromObjectId}/associations/${toObjectType}/${toObjectId}`,
      data: [
        {
          associationCategory: 'HUBSPOT_DEFINED',
          associationTypeId,
        },
      ],
    });
  }

  /**
   * Batch create associations
   */
  async batchCreateAssociations(
    fromObjectType: string,
    toObjectType: string,
    associations: BatchAssociation[]
  ): Promise<void> {
    await this.request({
      method: 'POST',
      url: `/crm/v4/associations/${fromObjectType}/${toObjectType}/batch/create`,
      data: { inputs: associations },
    });
  }

  // ===== Properties API =====

  /**
   * Get all properties for an object type
   */
  async getProperties(objectType: string): Promise<any[]> {
    const response = await this.request<{ results: any[] }>({
      method: 'GET',
      url: `/crm/v3/properties/${objectType}`,
    });
    return response.results;
  }

  /**
   * Create a custom property
   */
  async createProperty(objectType: string, property: any): Promise<any> {
    return this.request({
      method: 'POST',
      url: `/crm/v3/properties/${objectType}`,
      data: property,
    });
  }

  // ===== Pipelines API =====

  /**
   * Get all pipelines for an object type
   */
  async getPipelines(objectType: string): Promise<any[]> {
    const response = await this.request<{ results: any[] }>({
      method: 'GET',
      url: `/crm/v3/pipelines/${objectType}`,
    });
    return response.results;
  }

  // ===== Owners API =====

  /**
   * Get all owners
   */
  async getOwners(): Promise<any[]> {
    const response = await this.request<{ results: any[] }>({
      method: 'GET',
      url: '/crm/v3/owners',
    });
    return response.results;
  }

  // ===== Generic API request =====

  /**
   * Make a generic API request (for endpoints not covered above)
   */
  async apiRequest<T>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>(config);
  }
}
