/**
 * Request Timeout Utilities
 *
 * ISMS Policy: SC-002 (Secure Coding), PE-001 (Performance Standards)
 *
 * Provides timeout handling for long-running operations to prevent
 * resource exhaustion and ensure responsive API behavior.
 *
 * @see https://github.com/Hack23/ISMS-PUBLIC/blob/main/Secure_Development_Policy.md
 */

/**
 * Typed configuration for timeout operations.
 *
 * Centralises timeout settings in a single object so they can be
 * stored, passed, and validated without scattered magic numbers.
 */
export interface TimeoutConfig {
  /** Timeout duration in milliseconds (must be > 0) */
  timeoutMs: number;
  /** Optional human-readable label for the timed operation */
  operationName?: string;
  /** Custom error message override for timeout errors */
  errorMessage?: string;
}

/**
 * Default timeout configurations for common operation types.
 * These are compile-time constants; override by passing a custom {@link TimeoutConfig}
 * at the call site rather than relying on environment variables.
 */
export const DEFAULT_TIMEOUTS = {
  EP_API_REQUEST_MS: 60_000,
  EP_FEED_SLOW_REQUEST_MS: 120_000,
  HEALTH_CHECK_MS: 3_000,
  RETRY_DELAY_MS: 1_000,
} as const;

/**
 * Timeout error thrown when an operation exceeds its time limit
 *
 * @example
 * ```typescript
 * if (Date.now() - startTime > timeout) {
 *   throw new TimeoutError('Operation timed out after 10s');
 * }
 * ```
 */
export class TimeoutError extends Error {
  /**
   * Create a new timeout error
   *
   * @param message - Description of the timeout
   * @param timeoutMs - The timeout duration in milliseconds
   */
  constructor(
    message: string,
    public readonly timeoutMs?: number
  ) {
    super(message);
    this.name = 'TimeoutError';
    Error.captureStackTrace(this, this.constructor);
  }
}

/**
 * Execute a promise with a timeout
 *
 * Races the provided promise against a timeout. If the timeout expires
 * before the promise resolves, a TimeoutError is thrown.
 *
 * @template T - Type of the promise result
 * @param promise - Promise to execute with timeout
 * @param timeoutMs - Timeout in milliseconds
 * @param errorMessage - Optional custom error message
 * @returns Promise that resolves with the result or rejects with TimeoutError
 *
 * @throws {TimeoutError} If operation exceeds timeout
 *
 * @example
 * ```typescript
 * const result = await withTimeout(
 *   fetchFromAPI('/endpoint'),
 *   5000,
 *   'API request timed out'
 * );
 * ```
 *
 * @security
 * - Prevents resource exhaustion from hanging operations
 * - Ensures responsive API behavior
 * - Timeout values should be tuned per operation
 * @since 0.8.0
 */
export async function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number,
  errorMessage?: string
): Promise<T> {
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;

  const timeoutPromise = new Promise<never>((_, reject) => {
    timeoutHandle = setTimeout(() => {
      reject(
        new TimeoutError(
          errorMessage ?? `Operation timed out after ${String(timeoutMs)}ms`,
          timeoutMs
        )
      );
    }, timeoutMs);
  });

  try {
    return await Promise.race([promise, timeoutPromise]);
  } finally {
    if (timeoutHandle !== undefined) {
      clearTimeout(timeoutHandle);
    }
  }
}

/**
 * Wraps a promise with a timeout and optional AbortSignal support.
 *
 * For operations that support cancellation (like fetch), pass a function that
 * accepts an AbortSignal. The signal will be aborted when the timeout fires,
 * allowing the underlying operation to clean up resources.
 *
 * @template T - Type of the promise result
 * @param fn - Function that returns a promise and optionally accepts an AbortSignal
 * @param timeoutMs - Timeout in milliseconds
 * @param errorMessage - Custom error message (optional)
 * @returns Promise that resolves/rejects with the operation result or timeout
 * @throws {TimeoutError} If the operation exceeds `timeoutMs`
 *
 * @example
 * ```typescript
 * // With AbortSignal support (for fetch, etc.)
 * await withTimeoutAndAbort(
 *   (signal) => fetch(url, { signal }),
 *   5000,
 *   'API request timed out'
 * );
 * ```
 *
 * @security Aborts the underlying operation via `AbortController` when the
 *   timeout fires, preventing dangling fetch connections and resource leaks.
 *   Per ISMS Policy SC-002, all external network calls must be cancellable.
 * @since 0.8.0
 */
export async function withTimeoutAndAbort<T>(
  fn: (signal: AbortSignal) => Promise<T>,
  timeoutMs: number,
  errorMessage?: string
): Promise<T> {
  const controller = new AbortController();
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;

  const timeoutPromise = new Promise<never>((_, reject) => {
    timeoutHandle = setTimeout(() => {
      controller.abort();
      reject(
        new TimeoutError(
          errorMessage ?? `Operation timed out after ${String(timeoutMs)}ms`,
          timeoutMs
        )
      );
    }, timeoutMs);
  });

  try {
    const result = await Promise.race([fn(controller.signal), timeoutPromise]);
    return result;
  } finally {
    if (timeoutHandle !== undefined) {
      clearTimeout(timeoutHandle);
    }
  }
}

/**
 * Validate withRetry options
 *
 * @param maxRetries - Maximum retry count
 * @param timeoutMs - Timeout duration
 * @param retryDelayMs - Retry delay duration
 * @param maxDelayMs - Maximum delay cap
 * @throws {Error} If any option is invalid
 */
function validateRetryOptions(
  maxRetries: number,
  timeoutMs: number | undefined,
  retryDelayMs: number,
  maxDelayMs: number
): void {
  if (maxRetries < 0) {
    throw new Error('maxRetries must be non-negative');
  }
  if (timeoutMs !== undefined && timeoutMs <= 0) {
    throw new Error('timeoutMs must be positive');
  }
  if (!Number.isFinite(retryDelayMs) || retryDelayMs <= 0) {
    throw new Error('retryDelayMs must be positive');
  }
  if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0) {
    throw new Error('maxDelayMs must be positive');
  }
}

/**
 * Execute a function with retry logic and timeout
 *
 * Retries the operation up to {@link options.maxRetries} times (for a total
 * of maxRetries + 1 attempts including the initial call). Each retry has its
 * own timeout (if timeoutMs is provided).
 *
 * By default, all non-{@link TimeoutError} failures are considered retryable.
 * To restrict retries to transient failures only (for example, network
 * errors or 5xx status codes), provide a {@link options.shouldRetry}
 * predicate that returns true only for errors that should be retried.
 *
 * @template T - Type of the function result
 * @param fn - Async function to execute
 * @param options - Retry and timeout configuration
 * @param options.maxRetries - Maximum number of retry attempts after the initial call
 * @param options.timeoutMs - Optional per-attempt timeout in milliseconds (omit if fn handles timeout internally)
 * @param options.retryDelayMs - Base delay between retry attempts in milliseconds (default: 1000)
 * @param options.maxDelayMs - Maximum delay cap in milliseconds (default: 30000); prevents unbounded backoff growth
 * @param options.timeoutErrorMessage - Custom error message for timeout errors
 * @param options.shouldRetry - Predicate that decides if a failed attempt should be retried (default: retry all non-timeout errors)
 * @returns Promise that resolves with the result
 *
 * @throws {TimeoutError} If any attempt exceeds timeout
 * @throws {Error} If all retries are exhausted
 *
 * @example
 * ```typescript
 * // Retry up to 3 times (4 total attempts) on 5xx errors only with timeout
 * const data = await withRetry(
 *   () => fetchFromAPI('/endpoint'),
 *   {
 *     maxRetries: 3,
 *     timeoutMs: 5000,
 *     retryDelayMs: 1000,
 *     shouldRetry: (error) => error.statusCode >= 500
 *   }
 * );
 *
 * // Retry without additional timeout (fn handles timeout internally)
 * const data2 = await withRetry(
 *   () => withTimeoutAndAbort(signal => fetch(url, { signal }), 5000),
 *   {
 *     maxRetries: 3,
 *     retryDelayMs: 1000,
 *     shouldRetry: (error) => error.statusCode >= 500
 *   }
 * );
 * ```
 *
 * @security
 * - Prevents retry storms with exponential backoff
 * - Respects timeout limits per attempt (when provided)
 * - Configurable retry conditions for security
 * @since 0.8.0
 */
export async function withRetry<T>(
  fn: () => Promise<T>,
  options: {
    maxRetries: number;
    timeoutMs?: number;
    retryDelayMs?: number;
    maxDelayMs?: number;
    timeoutErrorMessage?: string;
    shouldRetry?: (error: unknown) => boolean;
  }
): Promise<T> {
  const {
    maxRetries,
    timeoutMs,
    retryDelayMs = 1000,
    maxDelayMs = 30_000,
    timeoutErrorMessage,
    shouldRetry = (): boolean => true
  } = options;

  validateRetryOptions(maxRetries, timeoutMs, retryDelayMs, maxDelayMs);

  let lastError: unknown;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const result = timeoutMs !== undefined
        ? await withTimeout(fn(), timeoutMs, timeoutErrorMessage)
        : await fn();
      return result;
    } catch (error: unknown) {
      lastError = error;

      if (error instanceof TimeoutError) {
        throw error;
      }

      if (!shouldRetry(error)) {
        throw error;
      }

      if (attempt < maxRetries) {
        const baseDelay = Math.min(retryDelayMs * Math.pow(2, attempt), maxDelayMs);
        const jitter = baseDelay * 0.1 * Math.random();
        await new Promise(resolve => setTimeout(resolve, baseDelay + jitter));
      }
    }
  }

  throw lastError;
}

/**
 * Type guard to check if an error is a TimeoutError
 *
 * @param error - Error to check
 * @returns true if error is a TimeoutError
 *
 * @example
 * ```typescript
 * try {
 *   await withTimeout(operation(), 5000);
 * } catch (error) {
 *   if (isTimeoutError(error)) {
 *     console.error('Operation timed out:', error.timeoutMs);
 *   }
 * }
 * ```
 *
 * @since 0.8.0
 */
export function isTimeoutError(error: unknown): error is TimeoutError {
  return error instanceof TimeoutError;
}

/**
 * Execute a promise with timeout settings from a {@link TimeoutConfig}.
 *
 * Convenience wrapper around {@link withTimeout} for callers that
 * already hold a `TimeoutConfig` object (e.g., from environment config).
 *
 * @template T - Type of the promise result
 * @param promise - Promise to execute with timeout
 * @param config  - Timeout configuration object
 * @returns Promise resolving with the result or rejecting with TimeoutError
 *
 * @throws {TimeoutError} If the operation exceeds `config.timeoutMs`
 * @throws {Error} If `config.timeoutMs` is not positive
 *
 * @example
 * ```typescript
 * const config: TimeoutConfig = { timeoutMs: 5000, operationName: 'fetchMEPs' };
 * const result = await withTimeoutConfig(fetchMEPs(), config);
 * ```
 *
 * @since 0.8.0
 */
export async function withTimeoutConfig<T>(
  promise: Promise<T>,
  config: TimeoutConfig
): Promise<T> {
  if (config.timeoutMs <= 0) {
    throw new Error(`withTimeoutConfig: timeoutMs must be > 0, got ${String(config.timeoutMs)}`);
  }
  const message =
    config.errorMessage ??
    `${config.operationName ?? 'Operation'} timed out after ${String(config.timeoutMs)}ms`;

  return withTimeout(promise, config.timeoutMs, message);
}
