/// Retry policy with exponential backoff for network requests
library retry_policy;

import 'dart:async';
import 'dart:io';
import 'dart:math';

/// Configuration for retry behavior
class RetryConfig {
  /// Maximum number of retry attempts
  final int maxAttempts;

  /// Initial delay before first retry (milliseconds)
  final int initialDelayMs;

  /// Maximum delay between retries (milliseconds)
  final int maxDelayMs;

  /// Multiplier for exponential backoff
  final double backoffMultiplier;

  /// Whether to add jitter to retry delays
  final bool useJitter;

  const RetryConfig({
    this.maxAttempts = 3,
    this.initialDelayMs = 1000,
    this.maxDelayMs = 30000,
    this.backoffMultiplier = 2.0,
    this.useJitter = true,
  });

  /// Default configuration for standard API calls
  static const standard = RetryConfig();

  /// Configuration for critical operations
  static const critical = RetryConfig(
    maxAttempts: 5,
    initialDelayMs: 500,
    maxDelayMs: 60000,
  );

  /// Configuration for background sync
  static const backgroundSync = RetryConfig(
    maxAttempts: 10,
    initialDelayMs: 2000,
    maxDelayMs: 120000,
  );
}

/// Retry policy implementation with exponential backoff
class RetryPolicy {
  final RetryConfig config;
  final Random _random = Random();

  RetryPolicy({RetryConfig? config}) : config = config ?? RetryConfig.standard;

  /// Executes an operation with retry logic
  ///
  /// [operation]: The async operation to execute
  /// [shouldRetry]: Optional function to determine if error is retryable
  /// [onRetry]: Optional callback when retry attempt is made
  ///
  /// Returns the result of the operation
  /// Throws the last error if all retries fail
  Future<T> execute<T>({
    required Future<T> Function() operation,
    bool Function(dynamic error)? shouldRetry,
    void Function(int attempt, dynamic error, Duration delay)? onRetry,
  }) async {
    int attempt = 0;
    dynamic lastError;

    while (attempt < config.maxAttempts) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        attempt++;

        // Check if we should retry this error
        final retryable = shouldRetry?.call(error) ?? _isRetryableError(error);

        if (!retryable || attempt >= config.maxAttempts) {
          throw error;
        }

        // Calculate delay with exponential backoff
        final delay = _calculateDelay(attempt);

        // Notify about retry
        onRetry?.call(attempt, error, delay);

        // Wait before next attempt
        await Future.delayed(delay);
      }
    }

    // Should never reach here, but throw last error just in case
    throw lastError!;
  }

  /// Calculates delay for given attempt number
  Duration _calculateDelay(int attempt) {
    // Exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
    final exponentialDelay = config.initialDelayMs *
        pow(config.backoffMultiplier, attempt - 1).toInt();

    // Cap at max delay
    var delayMs = min(exponentialDelay, config.maxDelayMs);

    // Add jitter to prevent thundering herd
    if (config.useJitter) {
      final jitter = _random.nextDouble() * 0.3; // ±30% jitter
      delayMs = (delayMs * (1 + jitter - 0.15)).toInt();
    }

    return Duration(milliseconds: delayMs);
  }

  /// Determines if an error is retryable
  bool _isRetryableError(dynamic error) {
    // Network errors are retryable
    if (error is TimeoutException) return true;
    if (error is SocketException) return true;

    // HTTP status codes that are retryable
    if (error.toString().contains('500')) return true; // Internal Server Error
    if (error.toString().contains('502')) return true; // Bad Gateway
    if (error.toString().contains('503')) return true; // Service Unavailable
    if (error.toString().contains('504')) return true; // Gateway Timeout
    if (error.toString().contains('429')) return true; // Too Many Requests

    // Client errors (4xx) are generally not retryable
    if (error.toString().contains('400')) return false; // Bad Request
    if (error.toString().contains('401')) return false; // Unauthorized
    if (error.toString().contains('403')) return false; // Forbidden
    if (error.toString().contains('404')) return false; // Not Found

    // Default: don't retry unknown errors
    return false;
  }
}

/// Extension to add retry capability to Future operations
extension RetryExtension<T> on Future<T> Function() {
  /// Executes this operation with retry logic
  Future<T> withRetry({
    RetryConfig? config,
    bool Function(dynamic error)? shouldRetry,
    void Function(int attempt, dynamic error, Duration delay)? onRetry,
  }) {
    final policy = RetryPolicy(config: config);
    return policy.execute(
      operation: this,
      shouldRetry: shouldRetry,
      onRetry: onRetry,
    );
  }
}
