import 'dart:async';

import 'package:dio/dio.dart';
import 'package:real_estate_mobile/config/app_config.dart';
import 'package:real_estate_mobile/core/constants/api_constants.dart';
import 'package:real_estate_mobile/core/network/api_exceptions.dart';
import 'package:real_estate_mobile/core/storage/secure_storage.dart';

class ApiClient {
  static ApiClient? _instance;
  late final Dio _dio;

  // Token refresh state
  bool _isRefreshing = false;
  final List<_QueuedRequest> _failedQueue = [];

  // Callback to notify app of forced logout
  static void Function()? onForceLogout;

  ApiClient._() {
    _dio = Dio(
      BaseOptions(
        baseUrl: AppConfig.apiBaseUrl,
        connectTimeout: const Duration(seconds: 30),
        receiveTimeout: const Duration(seconds: 30),
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      ),
    );

    _dio.interceptors.addAll([
      _authInterceptor(),
      _errorInterceptor(),
    ]);
  }

  static ApiClient get instance {
    _instance ??= ApiClient._();
    return _instance!;
  }

  Dio get dio => _dio;

  InterceptorsWrapper _authInterceptor() {
    return InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await SecureStorage.getAccessToken();
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        handler.next(options);
      },
    );
  }

  InterceptorsWrapper _errorInterceptor() {
    return InterceptorsWrapper(
      onError: (error, handler) async {
        // Handle 401 — attempt token refresh
        if (error.response?.statusCode == 401) {
          final requestUrl = error.requestOptions.path;

          // Don't try to refresh if this was the refresh request itself
          if (requestUrl.contains(ApiConstants.authRefresh)) {
            await _forceLogout();
            handler.reject(_wrapError(error));
            return;
          }

          // Don't refresh for auth endpoints (login, register, etc.)
          if (_isAuthEndpoint(requestUrl)) {
            handler.reject(_wrapError(error));
            return;
          }

          // If already refreshing, queue this request
          if (_isRefreshing) {
            try {
              final response = await _queueRequest(error.requestOptions);
              handler.resolve(response);
            } catch (e) {
              handler.reject(
                DioException(
                  requestOptions: error.requestOptions,
                  error: e,
                ),
              );
            }
            return;
          }

          // Attempt refresh
          _isRefreshing = true;

          try {
            final newAccessToken = await _refreshToken();

            if (newAccessToken != null) {
              // Retry original request with new token
              error.requestOptions.headers['Authorization'] =
                  'Bearer $newAccessToken';

              // Process queued requests
              _processQueue(null, newAccessToken);
              _isRefreshing = false;

              // Retry the original request
              final response = await _dio.fetch(error.requestOptions);
              handler.resolve(response);
              return;
            } else {
              // No refresh token available
              _processQueue(error);
              _isRefreshing = false;
              await _forceLogout();
              handler.reject(_wrapError(error));
              return;
            }
          } catch (refreshError) {
            // Refresh failed
            _processQueue(error);
            _isRefreshing = false;
            await _forceLogout();
            handler.reject(_wrapError(error));
            return;
          }
        }

        // Non-401 errors — parse as before
        handler.reject(_wrapError(error));
      },
    );
  }

  /// Attempt to refresh the access token using the stored refresh token.
  Future<String?> _refreshToken() async {
    final refreshToken = await SecureStorage.getRefreshToken();
    if (refreshToken == null) return null;

    // Use a separate Dio instance to avoid interceptor loops
    final refreshDio = Dio(
      BaseOptions(
        baseUrl: AppConfig.apiBaseUrl,
        connectTimeout: const Duration(seconds: 15),
        receiveTimeout: const Duration(seconds: 15),
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      ),
    );

    final response = await refreshDio.post(
      ApiConstants.authRefresh,
      data: {'refreshToken': refreshToken},
    );

    final data = response.data['data'] as Map<String, dynamic>;
    final newAccessToken = data['accessToken'] as String;
    final newRefreshToken = data['refreshToken'] as String;

    await SecureStorage.saveTokens(
      accessToken: newAccessToken,
      refreshToken: newRefreshToken,
    );

    return newAccessToken;
  }

  /// Queue a request to be retried after token refresh completes.
  Future<Response> _queueRequest(RequestOptions requestOptions) {
    final completer = Completer<Response>();
    _failedQueue.add(_QueuedRequest(
      completer: completer,
      requestOptions: requestOptions,
    ));
    return completer.future;
  }

  /// Process all queued requests after refresh succeeds or fails.
  void _processQueue(DioException? error, [String? newToken]) {
    for (final queued in _failedQueue) {
      if (error != null) {
        queued.completer.completeError(error);
      } else {
        // Update token and retry
        queued.requestOptions.headers['Authorization'] =
            'Bearer $newToken';
        _dio.fetch(queued.requestOptions).then(
              queued.completer.complete,
              onError: queued.completer.completeError,
            );
      }
    }
    _failedQueue.clear();
  }

  /// Force logout — clear tokens and notify the app.
  Future<void> _forceLogout() async {
    await SecureStorage.clearTokens();
    onForceLogout?.call();
  }

  /// Check if the URL is an auth endpoint that shouldn't trigger refresh.
  bool _isAuthEndpoint(String url) {
    const authPaths = [
      ApiConstants.authLogin,
      ApiConstants.authRegister,
      ApiConstants.authLogout,
      ApiConstants.authForgotPassword,
      ApiConstants.authResetPassword,
      ApiConstants.authVerifyEmail,
      ApiConstants.twoFactorVerify,
      ApiConstants.twoFactorVerifyBackup,
    ];
    return authPaths.any((path) => url.contains(path));
  }

  /// Wrap a DioException with parsed error data.
  DioException _wrapError(DioException error) {
    final response = error.response;
    if (response != null) {
      final data = response.data;
      if (data is Map<String, dynamic>) {
        final message = data['message'] is List
            ? (data['message'] as List).join(', ')
            : data['message']?.toString() ?? 'An error occurred';

        final errors = (data['errors'] as List<dynamic>?)
                ?.map(
                    (e) => FieldError.fromJson(e as Map<String, dynamic>))
                .toList() ??
            [];

        return DioException(
          requestOptions: error.requestOptions,
          response: error.response,
          error: ApiException(
            message: message,
            statusCode: response.statusCode,
            fieldErrors: errors,
          ),
        );
      }
    }

    return DioException(
      requestOptions: error.requestOptions,
      response: error.response,
      error: ApiException(
        message: error.message ?? 'Network error occurred',
        statusCode: response?.statusCode,
      ),
    );
  }
}

class _QueuedRequest {
  final Completer<Response> completer;
  final RequestOptions requestOptions;

  _QueuedRequest({
    required this.completer,
    required this.requestOptions,
  });
}
