/**
 * Retry Utilities
 *
 * Standard retry logic with exponential backoff and jitter for external API calls.
 * Consolidates duplicated retry patterns across WHMCS, Salesforce, and Freebit integrations.
 */

import { Logger } from "nestjs-pino";
import { extractErrorMessage } from "./error.util.js";

/**
 * Configuration for retry behavior
 */
export interface RetryOptions {
  /** Maximum number of retry attempts (default: 3) */
  maxAttempts?: number;
  /** Base delay in milliseconds (default: 1000) */
  baseDelayMs?: number;
  /** Maximum delay cap in milliseconds (default: 30000) */
  maxDelayMs?: number;
  /** Add random jitter to prevent thundering herd (default: true) */
  withJitter?: boolean;
  /** Function to determine if an error is retryable (default: always retry) */
  isRetryable?: (error: unknown) => boolean;
  /** Function to determine if error needs longer delay (e.g., rate limit) */
  needsLongerDelay?: (error: unknown) => number | null;
  /** Logger for debug/warn messages (optional) */
  logger?: Logger;
  /** Context label for logging */
  logContext?: string;
}

/**
 * Result of a retry operation
 */
export interface RetryResult<T> {
  success: boolean;
  data?: T;
  error?: Error;
  attempts: number;
}

const DEFAULT_OPTIONS: Required<
  Omit<RetryOptions, "logger" | "logContext" | "isRetryable" | "needsLongerDelay">
> = {
  maxAttempts: 3,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  withJitter: true,
};

/**
 * Calculate exponential backoff delay with optional jitter
 */
export function calculateBackoffDelay(
  attempt: number,
  baseDelayMs: number,
  maxDelayMs: number,
  withJitter: boolean
): number {
  // Exponential backoff: base * 2^(attempt-1)
  const exponentialDelay = baseDelayMs * Math.pow(2, attempt - 1);
  const cappedDelay = Math.min(exponentialDelay, maxDelayMs);

  // Add jitter (0-1 second) to prevent thundering herd
  const jitter = withJitter ? Math.random() * 1000 : 0;

  return Math.floor(cappedDelay + jitter);
}

/**
 * Promise-based sleep utility
 */
export async function sleep(ms: number): Promise<void> {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}

interface RetryDelayParams {
  attempt: number;
  error: unknown;
  baseDelayMs: number;
  maxDelayMs: number;
  withJitter: boolean;
  needsLongerDelay?: ((error: unknown) => number | null) | undefined;
}

/**
 * Determine delay before retry, considering rate limits
 */
function computeRetryDelay(params: RetryDelayParams): number {
  const { attempt, error, baseDelayMs, maxDelayMs, withJitter, needsLongerDelay } = params;
  let delay = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs, withJitter);

  if (!needsLongerDelay) {
    return delay;
  }

  const longerDelay = needsLongerDelay(error);
  if (longerDelay !== null) {
    delay = Math.max(delay, longerDelay);
  }

  return delay;
}

interface RetryLogParams {
  logger: Logger | undefined;
  logContext: string | undefined;
  attempt: number;
  maxAttempts: number;
  delay: number;
  error: unknown;
}

/**
 * Log a retry attempt if logger is provided
 */
function logRetryAttempt(params: RetryLogParams): void {
  const { logger, logContext, attempt, maxAttempts, delay, error } = params;

  if (!logger) {
    return;
  }

  logger.debug(`${logContext || "Operation"} failed, retrying in ${delay}ms`, {
    attempt,
    maxAttempts,
    delay,
    error: extractErrorMessage(error),
  });
}

/**
 * Execute a single retry attempt
 * Returns the result on success, or throws on unrecoverable failure
 */
async function executeAttempt<T>(
  operation: () => Promise<T>,
  attempt: number,
  maxAttempts: number,
  isRetryable: (error: unknown) => boolean
): Promise<{ success: true; data: T } | { success: false; error: Error; shouldRetry: boolean }> {
  try {
    const data = await operation();
    return { success: true, data };
  } catch (error) {
    const normalizedError = error instanceof Error ? error : new Error(String(error));
    const isLastAttempt = attempt === maxAttempts;
    const shouldRetry = !isLastAttempt && isRetryable(error);

    return { success: false, error: normalizedError, shouldRetry };
  }
}

/**
 * Execute an async operation with automatic retry on failure.
 *
 * @example
 * const result = await withRetry(
 *   () => api.fetchData(),
 *   {
 *     maxAttempts: 3,
 *     isRetryable: (err) => isNetworkError(err),
 *     logger: this.logger,
 *     logContext: 'fetchData',
 *   }
 * );
 */
export async function withRetry<T>(
  operation: () => Promise<T>,
  options: RetryOptions = {}
): Promise<T> {
  const {
    maxAttempts = DEFAULT_OPTIONS.maxAttempts,
    baseDelayMs = DEFAULT_OPTIONS.baseDelayMs,
    maxDelayMs = DEFAULT_OPTIONS.maxDelayMs,
    withJitter = DEFAULT_OPTIONS.withJitter,
    isRetryable = () => true,
    needsLongerDelay,
    logger,
    logContext,
  } = options;

  let lastError: Error = new Error("Unknown error");

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    // eslint-disable-next-line no-await-in-loop -- Retries must be sequential by design; each attempt must complete before deciding whether to retry
    const result = await executeAttempt(operation, attempt, maxAttempts, isRetryable);

    if (result.success) {
      return result.data;
    }

    lastError = result.error;

    if (!result.shouldRetry) {
      throw lastError;
    }

    const delay = computeRetryDelay({
      attempt,
      error: lastError,
      baseDelayMs,
      maxDelayMs,
      withJitter,
      needsLongerDelay,
    });
    logRetryAttempt({ logger, logContext, attempt, maxAttempts, delay, error: lastError });

    // eslint-disable-next-line no-await-in-loop -- Backoff delay must occur sequentially between retry attempts
    await sleep(delay);
  }

  // This should never be reached due to the throw in the loop,
  // but TypeScript requires a return
  throw lastError;
}

/**
 * Execute an async operation with retry, returning a result object instead of throwing.
 *
 * @example
 * const result = await withRetrySafe(() => api.fetchData());
 * if (result.success) {
 *   console.log(result.data);
 * } else {
 *   console.error(`Failed after ${result.attempts} attempts:`, result.error);
 * }
 */
export async function withRetrySafe<T>(
  operation: () => Promise<T>,
  options: RetryOptions = {}
): Promise<RetryResult<T>> {
  const maxAttempts = options.maxAttempts ?? DEFAULT_OPTIONS.maxAttempts;

  try {
    const data = await withRetry(operation, options);
    return {
      success: true,
      data,
      attempts: 1, // If successful, we don't track internal attempts here
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error(String(error)),
      attempts: maxAttempts,
    };
  }
}

/**
 * Common error type checkers for use with isRetryable option
 */
export const RetryableErrors = {
  /**
   * Check if error is a network/connection error
   */
  isNetworkError(error: unknown): boolean {
    if (!(error instanceof Error)) return false;
    const message = error.message.toLowerCase();
    return (
      message.includes("network") ||
      message.includes("timeout") ||
      message.includes("econnrefused") ||
      message.includes("econnreset") ||
      message.includes("socket hang up") ||
      message.includes("fetch failed")
    );
  },

  /**
   * Check if error is a rate limit error
   */
  isRateLimitError(error: unknown): boolean {
    if (!(error instanceof Error)) return false;
    const message = error.message.toLowerCase();
    const isRateLimited = message.includes("rate limit") || message.includes("too many requests");

    // Check for 429 status code
    const hasStatusCode =
      "statusCode" in error && typeof (error as { statusCode: unknown }).statusCode === "number";
    const is429 = hasStatusCode && (error as { statusCode: number }).statusCode === 429;

    return isRateLimited || is429;
  },

  /**
   * Check if error is a server error (5xx)
   */
  isServerError(error: unknown): boolean {
    if (!(error instanceof Error)) return false;

    const hasStatusCode =
      "statusCode" in error && typeof (error as { statusCode: unknown }).statusCode === "number";

    if (hasStatusCode) {
      const statusCode = (error as { statusCode: number }).statusCode;
      return statusCode >= 500 && statusCode < 600;
    }

    return false;
  },

  /**
   * Combined check: network, rate limit, or server errors
   */
  isTransientError(error: unknown): boolean {
    return (
      RetryableErrors.isNetworkError(error) ||
      RetryableErrors.isRateLimitError(error) ||
      RetryableErrors.isServerError(error)
    );
  },
};

/**
 * Get rate limit delay from error if applicable
 * Returns the delay in ms, or null if not a rate limit error
 */
export function getRateLimitDelay(error: unknown, defaultDelayMs = 30000): number | null {
  if (!RetryableErrors.isRateLimitError(error)) {
    return null;
  }

  // Check for Retry-After header value in error
  if (error instanceof Error && "retryAfter" in error) {
    const retryAfter = (error as { retryAfter: unknown }).retryAfter;
    if (typeof retryAfter === "number") {
      return retryAfter * 1000; // Convert seconds to ms
    }
  }

  return defaultDelayMs;
}
