/**
 * SDK Utility Functions
 * 
 * Helper functions for URL building, query parameters, and response handling
 */

import type { DatabaseId, ApiError } from './types';
import { createErrorFromApiResponse, NetworkError, TimeoutError } from './errors';

// ===== URL Building Utilities =====

/**
 * Build URL with path parameters
 */
export function buildUrl(baseUrl: string, path: string, params?: Record<string, string>): string {
  let url = `${baseUrl.replace(/\/$/, '')}${path}`;
  
  if (params) {
    for (const [key, value] of Object.entries(params)) {
      url = url.replace(`:${key}`, encodeURIComponent(value));
    }
  }
  
  return url;
}

/**
 * Build query string from parameters
 */
export function buildQueryString(params?: Record<string, unknown>): string {
  if (!params || Object.keys(params).length === 0) {
    return '';
  }
  
  const searchParams = new URLSearchParams();
  
  for (const [key, value] of Object.entries(params)) {
    if (value !== undefined && value !== null) {
      searchParams.append(key, value as string);
    }
  }
  
  const queryString = searchParams.toString();
  return queryString ? `?${queryString}` : '';
}

/**
 * Combine URL with query parameters
 */
export function buildFullUrl(
  baseUrl: string, 
  path: string, 
  params?: Record<string, string>,
  query?: Record<string, unknown> | object
): string {
  const url = buildUrl(baseUrl, path, params);
  const queryString = buildQueryString(query as Record<string, unknown>);
  return `${url}${queryString}`;
}

// ===== HTTP Request Utilities =====

/**
 * Default fetch options for API requests
 */
export const defaultFetchOptions: RequestInit = {
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  },
};

/**
 * Enhanced fetch with timeout and error handling
 */
export async function fetchWithTimeout(
  url: string,
  options: RequestInit & { timeout?: number } = {}
): Promise<Response> {
  const { timeout = 30000, ...fetchOptions } = options;
  
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      ...defaultFetchOptions,
      ...fetchOptions,
      signal: controller.signal,
    });
    
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error instanceof Error && error.name === 'AbortError') {
      throw new TimeoutError(`Request timeout after ${timeout}ms`);
    }
    
    throw new NetworkError('Network request failed', error);
  }
}

/**
 * Parse JSON response with error handling
 */
export async function parseJsonResponse<T>(response: Response): Promise<T> {
  const text = await response.text();
  
  if (!text) {
    throw new NetworkError('Empty response body');
  }
  
  try {
    const data = JSON.parse(text) as T | ApiError;
    
    // Check if response is an error
    if (!response.ok) {
      if (isApiError(data)) {
        throw createErrorFromApiResponse(data);
      }
      throw new NetworkError(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return data as T;
  } catch (error) {
    if (error instanceof Error && error.name === 'SyntaxError') {
      throw new NetworkError('Invalid JSON response', error);
    }
    throw error;
  }
}

/**
 * Type guard to check if response is an API error
 */
function isApiError(data: unknown): data is ApiError {
  if (
    typeof data === 'object' &&
    data !== null &&
    Object.prototype.hasOwnProperty.call(data, 'error')
  ) {
    const error = (data as Record<string, unknown>).error;
    if (
      typeof error === 'object' &&
      error !== null &&
      Object.prototype.hasOwnProperty.call(error, 'code') &&
      Object.prototype.hasOwnProperty.call(error, 'message')
    ) {
      return true;
    }
  }
  return false;
}

// ===== Validation Utilities =====

/**
 * Validate database ID
 */
export function validateDatabaseId(id: string): asserts id is DatabaseId {
  if (!id || typeof id !== 'string') {
    throw new Error('Database ID must be a non-empty string');
  }
}

/**
 * Validate URL format
 */
export function validateUrl(url: string): boolean {
  try {
    new URL(url);
    return true;
  } catch {
    return false;
  }
}

/**
 * Extract hostname from URL
 */
export function extractHostname(url: string): string {
  try {
    return new URL(url).hostname;
  } catch {
    throw new Error(`Invalid URL: ${url}`);
  }
}

// ===== Data Transformation Utilities =====

/**
 * Clean and normalize string data
 */
export function normalizeString(str: string | null | undefined): string {
  return (str ?? '').trim();
}

/**
 * Convert array to comma-separated string
 */
export function arrayToString(arr: string[]): string {
  return arr.filter(Boolean).join(', ');
}

/**
 * Safe number parsing
 */
export function parseNumber(value: string | number | null | undefined): number | undefined {
  if (typeof value === 'number') {
    return value;
  }
  
  if (typeof value === 'string') {
    const parsed = parseFloat(value);
    return isNaN(parsed) ? undefined : parsed;
  }
  
  return undefined;
}

// ===== Retry Utilities =====

/**
 * Retry configuration
 */
export interface RetryConfig {
  maxAttempts: number;
  baseDelay: number;
  maxDelay: number;
  backoffFactor: number;
}

/**
 * Default retry configuration
 */
export const defaultRetryConfig: RetryConfig = {
  maxAttempts: 3,
  baseDelay: 1000,
  maxDelay: 10000,
  backoffFactor: 2,
};

/**
 * Retry function with exponential backoff
 */
export async function withRetry<T>(
  fn: () => Promise<T>,
  config: Partial<RetryConfig> = {}
): Promise<T> {
  const { maxAttempts, baseDelay, maxDelay, backoffFactor } = {
    ...defaultRetryConfig,
    ...config,
  };
  
  let lastError: Error = new Error('No attempts made');
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
      
      if (attempt === maxAttempts) {
        break;
      }
      
      const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt - 1), maxDelay);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// ===== Debug Utilities =====

/**
 * Log request details for debugging
 */
export function logRequest(method: string, url: string, data?: unknown): void {
  if (process.env.NODE_ENV === 'development') {
    console.log(`[ScrapingSDK] ${method} ${url}`, data ? { data } : '');
  }
}

/**
 * Log response details for debugging
 */
export function logResponse(url: string, status: number, data?: unknown): void {
  if (process.env.NODE_ENV === 'development') {
    console.log(`[ScrapingSDK] Response ${status} from ${url}`, data ? { data } : '');
  }
} 