// Base API client for WordPress REST API communication

import { ApiError, ApiRequestError } from '../types/api';

/**
 * Default request timeout in milliseconds (30 seconds)
 */
const DEFAULT_TIMEOUT = 30_000;

/**
 * In-flight GET request deduplication map.
 * Key: "GET:/endpoint" -> Promise of the response.
 * Only GET requests are deduplicated since they are idempotent.
 */
const inflightRequests = new Map<string, Promise<unknown>>();

/**
 * Clear all in-flight request tracking (exposed for testing)
 */
export function _clearInflightRequests(): void {
  inflightRequests.clear();
}

/**
 * Retry configuration for API requests
 */
interface RetryConfig {
  /** Maximum number of retry attempts (default: 3) */
  maxRetries?: number;
  /** Base delay in milliseconds for exponential backoff (default: 1000) */
  baseDelay?: number;
  /** Maximum delay in milliseconds (default: 30000) */
  maxDelay?: number;
}

/**
 * HTTP status codes that should trigger a retry
 */
const RETRYABLE_STATUS_CODES = new Set([
  408, // Request Timeout
  429, // Too Many Requests (rate limit)
  500, // Internal Server Error
  502, // Bad Gateway
  503, // Service Unavailable
  504, // Gateway Timeout
]);

/**
 * Check if an error is retryable
 */
function isRetryableError(error: unknown): boolean {
  // Network errors are retryable
  if (error instanceof TypeError) {
    return true;
  }

  // Check ApiRequestError status codes
  if (error instanceof ApiRequestError) {
    return RETRYABLE_STATUS_CODES.has(error.status);
  }

  return false;
}

/**
 * Sleep for a specified duration
 */
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Calculate retry delay with exponential backoff and jitter
 */
function calculateRetryDelay(
  attempt: number,
  baseDelay: number,
  maxDelay: number,
  error?: unknown,
): number {
  // For rate limit errors, use a longer base delay
  if (error instanceof ApiRequestError && error.status === 429) {
    const rateLimitDelay = Math.min(maxDelay, Math.pow(2, attempt + 2) * 1000);
    return rateLimitDelay;
  }

  // Standard exponential backoff: 1s, 2s, 4s, 8s...
  const delay = Math.min(maxDelay, Math.pow(2, attempt) * baseDelay);

  // Add jitter (±25%) to prevent thundering herd
  const jitter = delay * 0.25 * (Math.random() * 2 - 1);

  return Math.round(delay + jitter);
}

/**
 * Format error message based on HTTP status code and API error response
 */
function formatErrorMessage(status: number, error?: ApiError): string {
  // Handle specific HTTP status codes
  switch (status) {
    case 401:
      return 'Your session has expired. Please refresh the page and log in again.';
    case 403:
      return 'You do not have permission to perform this action.';
    case 404:
      return 'The requested resource was not found.';
    case 400:
      // For validation errors, try to use the error message from API
      if (error?.message) {
        return `Invalid request: ${error.message}`;
      }
      return 'Invalid request. Please check your input and try again.';
    case 500:
    case 502:
    case 503:
      return 'An unexpected error occurred. Please try again.';
    default:
      return error?.message || 'An error occurred. Please try again.';
  }
}

/**
 * Options for apiRequest, extending standard RequestInit with additional controls.
 */
export interface ApiRequestOptions extends RequestInit {
  /** Request timeout in milliseconds (default: 30000). Set to 0 to disable. */
  timeout?: number;
}

/**
 * Make an authenticated request to the WordPress REST API
 * @param endpoint - API endpoint (e.g., '/workflows' or '/workflows/123')
 * @param options - Fetch request options (including optional timeout)
 * @returns Parsed JSON response
 */
export async function apiRequest<T = unknown>(
  endpoint: string,
  options: ApiRequestOptions = {},
): Promise<T> {
  const method = (options.method || 'GET').toUpperCase();
  const { timeout = DEFAULT_TIMEOUT, ...fetchOptions } = options;

  // Deduplicate identical in-flight GET requests
  if (method === 'GET') {
    const dedupeKey = `GET:${endpoint}`;
    const inflight = inflightRequests.get(dedupeKey);
    if (inflight) {
      return inflight as Promise<T>;
    }

    const requestPromise = _executeApiRequest<T>(endpoint, fetchOptions, timeout);

    inflightRequests.set(dedupeKey, requestPromise);

    // Clean up the inflight map when the request completes (success or failure).
    // The .catch(() => {}) prevents the .finally() chain from generating an
    // unhandled rejection warning - the actual rejection is still propagated
    // to callers via the original requestPromise reference.
    requestPromise
      .finally(() => {
        inflightRequests.delete(dedupeKey);
      })
      .catch(() => {});

    return requestPromise;
  }

  // Non-GET requests are never deduplicated
  return _executeApiRequest<T>(endpoint, fetchOptions, timeout);
}

/**
 * Sentinel value used to identify a timeout rejection in Promise.race.
 */
const TIMEOUT_SYMBOL = Symbol('timeout');

/**
 * Internal: Execute the actual API request with timeout support.
 * Uses Promise.race for timeout to avoid AbortSignal compatibility issues
 * between different JS environments (e.g., jsdom vs native Node).
 */
async function _executeApiRequest<T = unknown>(
  endpoint: string,
  options: RequestInit,
  timeout: number,
): Promise<T> {
  const fetchPromise = _executeFetch<T>(endpoint, options);

  if (timeout <= 0) {
    return fetchPromise;
  }

  // Race the fetch against a timeout
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
  const timeoutPromise = new Promise<typeof TIMEOUT_SYMBOL>((resolve) => {
    timeoutId = setTimeout(() => resolve(TIMEOUT_SYMBOL), timeout);
  });

  try {
    const result = await Promise.race([fetchPromise, timeoutPromise]);

    if (result === TIMEOUT_SYMBOL) {
      // Swallow the fetch promise rejection to prevent unhandled rejection warnings
      fetchPromise.catch(() => {});
      throw new Error(`Request to ${endpoint} timed out after ${timeout}ms`);
    }

    return result as T;
  } finally {
    if (timeoutId !== undefined) {
      clearTimeout(timeoutId);
    }
  }
}

/**
 * Internal: Execute the raw fetch call with URL building, headers, and error handling.
 */
async function _executeFetch<T = unknown>(endpoint: string, options: RequestInit): Promise<T> {
  try {
    // Build full URL - handle query params correctly for rest_route format
    const baseUrl = window.sequensyAPI.root;
    let url: string;

    if (endpoint.includes('?')) {
      // Endpoint has query params - need to merge correctly
      const [path, queryString] = endpoint.split('?');
      if (baseUrl.includes('?')) {
        // Base already has query string (rest_route format) - use & to join
        url = `${baseUrl}${path}&${queryString}`;
      } else {
        // Base is clean URL (pretty permalinks) - use ? as normal
        url = `${baseUrl}${endpoint}`;
      }
    } else {
      url = `${baseUrl}${endpoint}`;
    }

    // Prepare headers
    const headers: HeadersInit = {
      'Content-Type': 'application/json',
      'X-WP-Nonce': window.sequensyAPI.nonce,
      ...options.headers,
    };

    // Make request
    const response = await fetch(url, {
      ...options,
      headers,
    });

    // Handle HTTP errors
    if (!response.ok) {
      let error: ApiError | undefined;

      try {
        // Try to parse WordPress REST API error format
        error = await response.json();
      } catch (_e) {
        // Response body is not JSON, use default error
        console.error('API request failed:', response.statusText);
      }

      const errorMessage = formatErrorMessage(response.status, error);
      console.error('API Error:', {
        status: response.status,
        endpoint,
        error,
      });

      // Throw custom error that preserves full response data
      throw new ApiRequestError(
        errorMessage,
        error?.code || 'unknown_error',
        response.status,
        error?.data,
      );
    }

    // Parse and return JSON response
    return await response.json();
  } catch (err) {
    // Handle network errors
    if (err instanceof TypeError) {
      console.error('Network error:', err);
      throw new Error('Could not connect to server. Please check your connection.');
    }

    // Re-throw other errors (including formatted HTTP errors)
    throw err;
  }
}

/**
 * Make an API request with automatic retry on transient failures.
 *
 * Uses exponential backoff for retries. Only retries on network errors
 * and specific HTTP status codes (429, 500, 502, 503, 504, etc.)
 *
 * @param endpoint - API endpoint
 * @param options - Fetch request options
 * @param retryConfig - Retry configuration
 * @returns Parsed JSON response
 */
export async function apiRequestWithRetry<T = unknown>(
  endpoint: string,
  options: ApiRequestOptions = {},
  retryConfig: RetryConfig = {},
): Promise<T> {
  const { maxRetries = 3, baseDelay = 1000, maxDelay = 30000 } = retryConfig;

  let lastError: unknown;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await apiRequest<T>(endpoint, options);
    } catch (error) {
      lastError = error;

      // Don't retry non-retryable errors
      if (!isRetryableError(error)) {
        throw error;
      }

      // Don't retry after last attempt
      if (attempt < maxRetries) {
        const delay = calculateRetryDelay(attempt, baseDelay, maxDelay, error);

        console.warn(
          `API request to ${endpoint} failed (attempt ${attempt + 1}/${maxRetries + 1}). ` +
            `Retrying in ${delay}ms...`,
          error instanceof Error ? error.message : error,
        );

        await sleep(delay);
      }
    }
  }

  // All retries exhausted - throw the last error
  throw lastError;
}
