import 'dart:async';

import 'package:dio/dio.dart';

import '../../services/token_storage.dart';

/// 갱신 토큰을 기반으로 새로운 토큰 쌍을 받아오는 비동기 콜백 시그니처.
typedef RefreshTokenCallback = Future<TokenPair?> Function();

/// 액세스/리프레시 토큰 값을 함께 보관하기 위한 불변 모델.
class TokenPair {
  /// [accessToken], [refreshToken]을 모두 전달받아 초기화한다.
  const TokenPair({required this.accessToken, required this.refreshToken});

  /// 인증 헤더에 사용되는 액세스 토큰 문자열.
  final String accessToken;

  /// 토큰 재발급 요청에 전달되는 리프레시 토큰 문자열.
  final String refreshToken;
}

/// 인증 인터셉터
/// - 요청 전에 Authorization 헤더 주입
/// - 401 수신 시 토큰 갱신 및 원요청 1회 재시도
class AuthInterceptor extends Interceptor {
  AuthInterceptor({
    required TokenStorage tokenStorage,
    required Dio dio,
    this.onRefresh,
  }) : _tokenStorage = tokenStorage,
       _dio = dio;

  final TokenStorage _tokenStorage;
  final Dio _dio;

  /// 서버에 토큰 재발급을 요청하는 콜백. null이면 갱신을 시도하지 않는다.
  final RefreshTokenCallback? onRefresh;

  /// 동시에 들어온 요청을 순차적으로 처리하기 위한 대기열.
  final List<Completer<void>> _refreshQueue = [];

  /// 현재 토큰 갱신 중인지 여부.
  bool _isRefreshing = false;

  /// 요청 직전에 저장된 액세스 토큰을 Authorization 헤더에 주입한다.
  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    final token = await _tokenStorage.readAccessToken();
    if (token != null && token.isNotEmpty) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  /// 401 응답을 감지하면 토큰을 갱신하고 동일 요청을 한 번 재시도한다.
  @override
  Future<void> onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) async {
    if (!_shouldAttemptRefresh(err)) {
      handler.next(err);
      return;
    }

    try {
      await _refreshToken();
      final response = await _retry(err.requestOptions);
      handler.resolve(response);
    } on _RefreshFailedException {
      await _tokenStorage.clear();
      handler.next(err);
    } on DioException catch (e) {
      handler.next(e);
    } catch (_) {
      handler.next(err);
    }
  }

  /// 토큰 갱신을 시도해야 하는 상황인지 판별한다.
  bool _shouldAttemptRefresh(DioException err) {
    return onRefresh != null &&
        err.response?.statusCode == 401 &&
        err.requestOptions.extra['__retry'] != true;
  }

  /// 토큰을 갱신하고 대기 중인 요청을 깨운다.
  Future<void> _refreshToken() async {
    if (_isRefreshing) {
      final completer = Completer<void>();
      _refreshQueue.add(completer);
      return completer.future;
    }

    _isRefreshing = true;
    try {
      final callback = onRefresh;
      if (callback == null) {
        throw const _RefreshFailedException();
      }

      final pair = await callback();
      if (pair == null) {
        throw const _RefreshFailedException();
      }

      await _tokenStorage.writeAccessToken(pair.accessToken);
      await _tokenStorage.writeRefreshToken(pair.refreshToken);
    } finally {
      _isRefreshing = false;
      for (final completer in _refreshQueue) {
        if (!completer.isCompleted) {
          completer.complete();
        }
      }
      _refreshQueue.clear();
    }
  }

  /// 최신 토큰을 헤더에 주입해 한 번만 동일 요청을 재시도한다.
  Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
    final token = await _tokenStorage.readAccessToken();
    if (token != null && token.isNotEmpty) {
      requestOptions.headers['Authorization'] = 'Bearer $token';
    } else {
      requestOptions.headers.remove('Authorization');
    }
    requestOptions.extra['__retry'] = true;
    return _dio.fetch(requestOptions);
  }
}

/// 토큰 재발급 실패 시 재시도 루프를 중단하기 위해 사용하는 내부 예외.
class _RefreshFailedException implements Exception {
  const _RefreshFailedException();
}
