import 'dart:async';

import '../../core.dart';

/// Callback to refresh the authentication token.
/// Typically used to request a new token from the server.
typedef RefreshTokenCallback = Future<String?> Function();

/// Callback to save a new authentication token.
typedef SaveTokenCallback = Future<void> Function(String token);

/// Callback to clear the authentication token, e.g., on logout or failure.
typedef ClearTokenCallback = Future<void> Function();

/// Callback invoked when token refresh fails.
/// Typically used to redirect the user to login or show a logout message.
typedef OnRefreshFailedCallback = Future<void> Function();

/// Represents a queued request waiting for token refresh.
class QueuedRequest {
  /// The original request options.
  final RequestOptions options;

  /// Completer used to complete the response once the request is retried.
  final Completer<Response> completer;

  /// Constructs a queued request.
  QueuedRequest(this.options, this.completer);
}

/// An interceptor for automatic token management and refresh handling.
///
/// Features:
/// - Queues requests while a token refresh is in progress.
/// - Saves and clears tokens via provided callbacks.
/// - Calls [OnRefreshFailedCallback] if token refresh fails.
class AppInterceptorN extends Interceptor {
  /// Callback to refresh the authentication token.
  final RefreshTokenCallback? refreshTokenCallback;

  /// Callback to save the new token.
  final SaveTokenCallback saveTokenCallback;

  /// Callback to clear the token.
  final ClearTokenCallback clearTokenCallback;

  /// Callback executed when token refresh fails.
  final OnRefreshFailedCallback onRefreshFailed;

  /// Optional additional arguments for authentication.
  final dynamic authArguments;

  /// The Dio instance used to send requests.
  final Dio dio;

  /// Maximum number of retry attempts for failed requests.
  final int maxRetries;

  /// Whether a token refresh is currently in progress.
  bool _isRefreshing = false;

  /// Queue of requests waiting for a new token.
  final List<QueuedRequest> _queue = [];

  /// Current token in use.
  String? _currentToken;

  /// Constructs the interceptor.
  AppInterceptorN({
    required this.dio,
    required this.saveTokenCallback,
    required this.clearTokenCallback,
    required this.onRefreshFailed,
    this.refreshTokenCallback,
    this.authArguments,
    this.maxRetries = 3,
  });

  /// Called before sending a request.
  /// If a token refresh is in progress, the request is added to the queue.
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    if (_isRefreshing) {
      final completer = Completer<Response>();
      _queue.add(QueuedRequest(options, completer));
      return handler.resolve(await completer.future);
    }
    handler.next(options);
  }

  /// Called when an error occurs during a request.
  ///
  /// - If the error is a 401 (unauthorized) and retry count is below `maxRetries`,
  ///   the token is refreshed and queued requests are retried.
  /// - If the token refresh fails, all queued requests are cancelled and
  ///   [onRefreshFailed] is executed.
  @override
  Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
    int currentRetry = err.requestOptions.extra['retryCount'] ?? 0;

    if (err.response?.statusCode == 401 &&
        err.type != DioExceptionType.cancel &&
        currentRetry < maxRetries) {
      final completer = Completer<Response>();
      final updatedOptions = err.requestOptions.copyWith(
        extra: {...err.requestOptions.extra, 'retryCount': currentRetry + 1},
      );
      _queue.add(QueuedRequest(updatedOptions, completer));

      if (!_isRefreshing) {
        _isRefreshing = true;
        try {
          final newToken = await refreshTokenCallback?.call();
          if (newToken != null && newToken.isNotEmpty) {
            _currentToken = newToken;
            await saveTokenCallback(newToken);

            for (var req in _queue) {
              final newOptions = req.options.copyWith(
                headers: {...req.options.headers, 'Authorization': 'Bearer $newToken'},
              );
              dio
                  .fetch(newOptions)
                  .then(req.completer.complete)
                  .catchError(req.completer.completeError);
            }
          } else {
            await clearTokenCallback();
            await _handleRefreshFailure();
            for (var req in _queue) {
              req.completer.completeError(
                DioException(requestOptions: req.options, type: DioExceptionType.cancel),
              );
            }
          }
        } catch (e) {
          await clearTokenCallback();
          await _handleRefreshFailure();
          for (var req in _queue) {
            req.completer.completeError(e);
          }
        } finally {
          _queue.clear();
          _isRefreshing = false;
        }
      }

      return handler.resolve(await completer.future);
    }

    handler.next(err);
  }

  /// Handles token refresh failure:
  /// - Cancels all ongoing requests via [ApiHandler].
  /// - Executes external [onRefreshFailed] callback.
  Future<void> _handleRefreshFailure() async {
    ApiHandler.cancelAllRequests("Token refresh failed");

    await onRefreshFailed.call();
  }

  @visibleForTesting
  set isRefreshingForTest(bool value) => _isRefreshing = value;

  @visibleForTesting
  List<QueuedRequest> get queue => _queue;
}
