import axios, { AxiosInstance, AxiosError } from 'axios';
import type { QBOEntity, QueryResponse, QBOError } from '../types/index.js';

export interface QuickBooksConfig {
  accessToken: string;
  realmId: string;
  refreshToken?: string;
  sandbox?: boolean;
  minorVersion?: number;
}

export interface PaginationOptions {
  startPosition?: number;
  maxResults?: number;
}

export interface QueryOptions extends PaginationOptions {
  sql: string;
}

const DEFAULT_MINOR_VERSION = 73;
const MAX_RESULTS = 1000;
const RATE_LIMIT_REQUESTS = 500;
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
const MAX_RETRIES = 3;
const INITIAL_RETRY_DELAY_MS = 1000;

export class QuickBooksClient {
  private client: AxiosInstance;
  private realmId: string;
  private refreshToken?: string;
  private minorVersion: number;
  private requestCount = 0;
  private windowStart = Date.now();

  constructor(config: QuickBooksConfig) {
    this.realmId = config.realmId;
    this.refreshToken = config.refreshToken;
    this.minorVersion = config.minorVersion ?? DEFAULT_MINOR_VERSION;

    const baseURL = config.sandbox
      ? 'https://sandbox-quickbooks.api.intuit.com'
      : 'https://quickbooks.api.intuit.com';

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

  /**
   * Rate limiting check - respects QBO 500 req/min throttle
   */
  private async checkRateLimit(): Promise<void> {
    const now = Date.now();
    const elapsed = now - this.windowStart;

    if (elapsed >= RATE_LIMIT_WINDOW_MS) {
      // Reset window
      this.requestCount = 0;
      this.windowStart = now;
      return;
    }

    if (this.requestCount >= RATE_LIMIT_REQUESTS) {
      // Wait until window resets
      const waitMs = RATE_LIMIT_WINDOW_MS - elapsed;
      await this.sleep(waitMs);
      this.requestCount = 0;
      this.windowStart = Date.now();
    }

    this.requestCount++;
  }

  /**
   * Exponential backoff retry logic
   */
  private async retry<T>(
    fn: () => Promise<T>,
    retries = MAX_RETRIES,
    delay = INITIAL_RETRY_DELAY_MS
  ): Promise<T> {
    try {
      await this.checkRateLimit();
      return await fn();
    } catch (error) {
      if (retries === 0) throw error;

      const axiosError = error as AxiosError;
      
      // Retry on rate limit (429) or server errors (5xx)
      if (
        axiosError.response?.status === 429 ||
        (axiosError.response?.status && axiosError.response.status >= 500)
      ) {
        await this.sleep(delay);
        return this.retry(fn, retries - 1, delay * 2);
      }

      throw error;
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * Build URL with minor version parameter
   */
  private buildUrl(path: string, params?: Record<string, string | number>): string {
    const url = `/v3/company/${this.realmId}${path}`;
    const queryParams = new URLSearchParams({
      minorversion: this.minorVersion.toString(),
      ...Object.fromEntries(
        Object.entries(params || {}).map(([k, v]) => [k, String(v)])
      ),
    });
    return `${url}?${queryParams.toString()}`;
  }

  /**
   * Execute SQL-like query
   */
  async query<T extends QBOEntity>(
    sql: string,
    options: PaginationOptions = {}
  ): Promise<QueryResponse<T>> {
    const { startPosition = 1, maxResults = MAX_RESULTS } = options;
    
    // QBO uses STARTPOSITION and MAXRESULTS in the SQL query itself
    const paginatedSql = `${sql} STARTPOSITION ${startPosition} MAXRESULTS ${Math.min(maxResults, MAX_RESULTS)}`;

    return this.retry(async () => {
      const url = this.buildUrl('/query', { query: paginatedSql });
      const response = await this.client.get<QueryResponse<T>>(url);
      return response.data;
    });
  }

  /**
   * Create entity
   */
  async create<T extends QBOEntity>(
    entityType: string,
    data: Partial<T>
  ): Promise<T> {
    return this.retry(async () => {
      const url = this.buildUrl(`/${entityType.toLowerCase()}`);
      const response = await this.client.post<{ [key: string]: T }>(url, data);
      return response.data[entityType];
    });
  }

  /**
   * Read entity by ID
   */
  async read<T extends QBOEntity>(
    entityType: string,
    id: string
  ): Promise<T> {
    return this.retry(async () => {
      const url = this.buildUrl(`/${entityType.toLowerCase()}/${id}`);
      const response = await this.client.get<{ [key: string]: T }>(url);
      return response.data[entityType];
    });
  }

  /**
   * Update entity (requires SyncToken for optimistic locking)
   */
  async update<T extends QBOEntity>(
    entityType: string,
    data: Partial<T> & { Id: string; SyncToken: string }
  ): Promise<T> {
    return this.retry(async () => {
      const url = this.buildUrl(`/${entityType.toLowerCase()}`);
      const response = await this.client.post<{ [key: string]: T }>(url, data);
      return response.data[entityType];
    });
  }

  /**
   * Delete entity (soft delete - requires SyncToken)
   */
  async delete<T extends QBOEntity>(
    entityType: string,
    id: string,
    syncToken: string
  ): Promise<T> {
    return this.retry(async () => {
      const url = this.buildUrl(`/${entityType.toLowerCase()}`, {
        operation: 'delete',
      });
      const response = await this.client.post<{ [key: string]: T }>(url, {
        Id: id,
        SyncToken: syncToken,
      });
      return response.data[entityType];
    });
  }

  /**
   * Get report
   */
  async getReport<T = any>(
    reportName: string,
    params: Record<string, string | number> = {}
  ): Promise<T> {
    return this.retry(async () => {
      const url = this.buildUrl(`/reports/${reportName}`, params);
      const response = await this.client.get<T>(url);
      return response.data;
    });
  }

  /**
   * Refresh access token using refresh token
   */
  async refreshAccessToken(
    clientId: string,
    clientSecret: string
  ): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
    if (!this.refreshToken) {
      throw new Error('No refresh token available');
    }

    const tokenUrl = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer';
    
    const response = await axios.post(
      tokenUrl,
      new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: this.refreshToken,
      }),
      {
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        auth: {
          username: clientId,
          password: clientSecret,
        },
      }
    );

    // Update client with new access token
    this.client.defaults.headers['Authorization'] = `Bearer ${response.data.access_token}`;
    this.refreshToken = response.data.refresh_token;

    return response.data;
  }

  /**
   * Get company info
   */
  async getCompanyInfo(): Promise<any> {
    return this.retry(async () => {
      const url = this.buildUrl('/companyinfo', { id: this.realmId });
      const response = await this.client.get(url);
      return response.data.CompanyInfo;
    });
  }

  /**
   * Get preferences
   */
  async getPreferences(): Promise<any> {
    return this.retry(async () => {
      const url = this.buildUrl('/preferences');
      const response = await this.client.get(url);
      return response.data.Preferences;
    });
  }

  /**
   * Batch operations (up to 30 operations per batch)
   */
  async batch(operations: Array<{
    bId: string;
    operation: 'create' | 'update' | 'delete' | 'query';
    entity?: string;
    data?: any;
    query?: string;
  }>): Promise<any> {
    return this.retry(async () => {
      const url = this.buildUrl('/batch');
      const response = await this.client.post(url, {
        BatchItemRequest: operations.map(op => {
          if (op.operation === 'query') {
            return {
              bId: op.bId,
              Query: op.query,
            };
          }
          return {
            bId: op.bId,
            operation: op.operation,
            [op.entity!]: op.data,
          };
        }),
      });
      return response.data.BatchItemResponse;
    });
  }
}
