import 'package:dio/dio.dart';

import '../sessao/sessao_store.dart';

/// Erro de API já traduzido para mensagem legível.
///
/// O backend manda motivo distinguível em cada recusa (prazo fechado, bolão já
/// começou, convite expirado). Jogar "erro genérico" na tela desperdiça isso e
/// é o que faz o participante desistir e voltar ao WhatsApp.
class ApiError implements Exception {
  ApiError(this.mensagem, {this.status, this.semConexao = false});

  final String mensagem;
  final int? status;
  final bool semConexao;

  @override
  String toString() => mensagem;
}

/// Sinaliza que a sessão morreu e o app precisa voltar ao login.
class SessaoExpirada extends ApiError {
  SessaoExpirada() : super('sua sessão expirou', status: 401);
}

class ApiClient {
  ApiClient(this._sessao, {Dio? dio, String? baseUrl}) : _dio = dio ?? Dio() {
    _dio.options
      ..baseUrl = baseUrl ?? padraoBaseUrl
      ..connectTimeout = const Duration(seconds: 10)
      ..receiveTimeout = const Duration(seconds: 15)
      // Não lança em 4xx: o app decide o que fazer com cada motivo.
      ..validateStatus = (s) => s != null && s < 500;

    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) async {
          final token = await _sessao.accessToken;
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          handler.next(options);
        },
        onResponse: (response, handler) async {
          // 401 com refresh disponível: tenta renovar uma vez e repetir.
          if (response.statusCode == 401 && !_renovando) {
            final renovou = await _tentarRenovar();
            if (renovou) {
              final repetida = await _repetir(response.requestOptions);
              return handler.resolve(repetida);
            }
          }
          handler.next(response);
        },
      ),
    );
  }

  /// Endereço da API.
  ///
  /// `10.0.2.2` é como o emulador Android alcança o localhost da máquina. Em
  /// **device físico** isso não existe: passe o IP da máquina na rede ao
  /// compilar, senão o app fica tentando um endereço que só o emulador entende.
  ///
  /// ```
  /// flutter run --dart-define=API_URL=http://192.168.0.109:3000
  /// ```
  static const padraoBaseUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'http://10.0.2.2:3000',
  );

  final Dio _dio;
  final SessaoStore _sessao;
  bool _renovando = false;

  Future<bool> _tentarRenovar() async {
    final refresh = await _sessao.refreshToken;
    if (refresh == null) return false;

    _renovando = true;
    try {
      final r = await _dio.post<Map<String, dynamic>>(
        '/auth/refresh',
        data: {'refreshToken': refresh},
      );
      if (r.statusCode == 200 && r.data != null) {
        await _sessao.salvar(
          accessToken: r.data!['accessToken'] as String,
          refreshToken: r.data!['refreshToken'] as String,
        );
        return true;
      }
      // Refresh recusado significa sessão revogada — some com a credencial.
      await _sessao.limpar();
      return false;
    } on DioException {
      return false;
    } finally {
      _renovando = false;
    }
  }

  Future<Response<dynamic>> _repetir(RequestOptions o) => _dio.request(
    o.path,
    data: o.data,
    queryParameters: o.queryParameters,
    options: Options(method: o.method, headers: o.headers),
  );

  Future<T> get<T>(String path, {Map<String, dynamic>? query}) =>
      _executar<T>(() => _dio.get<T>(path, queryParameters: query));

  Future<T> post<T>(String path, {Object? body}) =>
      _executar<T>(() => _dio.post<T>(path, data: body));

  Future<T> patch<T>(String path, {Object? body}) =>
      _executar<T>(() => _dio.patch<T>(path, data: body));

  Future<T> delete<T>(String path) => _executar<T>(() => _dio.delete<T>(path));

  Future<T> _executar<T>(Future<Response<T>> Function() chamada) async {
    late Response<T> r;
    try {
      r = await chamada();
    } on DioException catch (e) {
      final semRede =
          e.type == DioExceptionType.connectionError ||
          e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.receiveTimeout;
      throw ApiError(
        semRede
            ? 'sem conexão com o servidor'
            : 'não foi possível concluir a operação',
        semConexao: semRede,
      );
    }

    final status = r.statusCode ?? 0;
    if (status >= 200 && status < 300) return r.data as T;
    if (status == 401) throw SessaoExpirada();

    throw ApiError(
      _mensagemDe(r.data) ?? 'não foi possível concluir',
      status: status,
    );
  }

  /// Extrai a mensagem que o backend mandou. Ela é escrita para o usuário ler.
  String? _mensagemDe(dynamic data) {
    if (data is Map && data['message'] != null) {
      final m = data['message'];
      if (m is List) return m.join('\n');
      return m.toString();
    }
    return null;
  }
}
