part of '../app/app.dart';

class ApiClient {
  ApiClient({required this.baseUrl, this.token});

  final String baseUrl;
  String? token;

  Uri _uri(String path, [Map<String, String>? query]) {
    final normalizedBase = normalizeBaseUrl(baseUrl);
    final base = Uri.parse(normalizedBase);
    return base.replace(
      path: '${base.path.replaceAll(RegExp(r'/$'), '')}/$path',
      queryParameters: query,
    );
  }

  Map<String, String> get _headers => {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    if (token != null) 'Authorization': 'Bearer $token',
  };

  Future<Map<String, dynamic>> post(
    String path,
    Map<String, dynamic> body,
  ) async {
    return _sendWithRenew(
      path,
      (client, uri) => client
          .post(uri, headers: _headers, body: jsonEncode(body))
          .timeout(const Duration(seconds: 15)),
    );
  }

  Future<Map<String, dynamic>> get(
    String path, [
    Map<String, String>? query,
  ]) async {
    return _sendWithRenew(
      path,
      (client, uri) => client
          .get(uri, headers: _headers)
          .timeout(const Duration(seconds: 15)),
      query,
    );
  }

  Future<Map<String, dynamic>> _sendWithRenew(
    String path,
    Future<http.Response> Function(http.Client client, Uri uri) send, [
    Map<String, String>? query,
  ]) async {
    final uri = _uri(path, query);
    try {
      return await _sendOnce(uri, send);
    } on ApiException catch (error) {
      if (!_canRenew(path, error)) {
        rethrow;
      }
      final renewed = await _renewToken(uri);
      if (!renewed) {
        rethrow;
      }
      debugPrint('MyLIS session diperbarui, mengulang request: $uri');
      return _sendOnce(uri, send);
    }
  }

  Future<Map<String, dynamic>> _sendOnce(
    Uri uri,
    Future<http.Response> Function(http.Client client, Uri uri) send,
  ) async {
    final client = _clientFor(uri);
    try {
      final response = await send(client, uri);
      return _decode(response, uri);
    } catch (error, stackTrace) {
      throw _connectionException(uri, error, stackTrace);
    } finally {
      client.close();
    }
  }

  bool _canRenew(String path, ApiException error) {
    return token != null &&
        error.statusCode == 401 &&
        path != 'api/mobile/login' &&
        !path.endsWith('/login');
  }

  Future<bool> _renewToken(Uri failedUri) async {
    final credentials = await SessionStore.credentials();
    if (credentials == null) {
      debugPrint(
        'MyLIS session expired untuk $failedUri; kredensial tersimpan tidak ada.',
      );
      return false;
    }

    final loginUri = _uri('api/mobile/login');
    final client = _clientFor(loginUri);
    try {
      debugPrint(
        'MyLIS session expired untuk $failedUri; mencoba renew token.',
      );
      final response = await client
          .post(
            loginUri,
            headers: const {
              'Accept': 'application/json',
              'Content-Type': 'application/json',
            },
            body: jsonEncode({
              'username': credentials.username,
              'password': credentials.password,
            }),
          )
          .timeout(const Duration(seconds: 15));
      final data = _decode(response, loginUri);
      final token = data['token']?.toString() ?? '';
      if (token.isEmpty) {
        debugPrint(
          'MyLIS renew token gagal: respons login tidak berisi token.',
        );
        return false;
      }
      this.token = token;
      await SessionStore.saveLogin(
        baseUrl: baseUrl,
        token: token,
        username: credentials.username,
        password: credentials.password,
      );
      return true;
    } on ApiException catch (error) {
      debugPrint('MyLIS renew token gagal: ${error.message}');
      return false;
    } catch (error, stackTrace) {
      debugPrint('MyLIS renew token gagal tidak terduga: $error');
      debugPrintStack(stackTrace: stackTrace);
      return false;
    } finally {
      client.close();
    }
  }

  http.Client _clientFor(Uri uri) {
    final client = HttpClient()
      ..badCertificateCallback = (certificate, host, port) {
        debugPrint(
          'MyLIS menerima sertifikat internal untuk $host:$port '
          'issuer=${certificate.issuer}',
        );
        return true;
      };
    return IOClient(client);
  }

  Map<String, dynamic> _decode(http.Response response, Uri uri) {
    final bodyPreview = response.body.length > 240
        ? '${response.body.substring(0, 240)}...'
        : response.body;

    if (response.statusCode < 200 || response.statusCode >= 300) {
      Object? decoded;
      if (_looksLikeJson(response)) {
        try {
          decoded = response.body.isEmpty
              ? <String, dynamic>{}
              : jsonDecode(response.body);
        } catch (_) {
          decoded = null;
        }
      }
      final message = decoded is Map ? decoded['message'] : null;
      debugPrint(
        'MyLIS API gagal: ${response.request?.method ?? '-'} $uri '
        'HTTP ${response.statusCode}; body=$bodyPreview',
      );
      throw ApiException(
        message?.toString() ??
            'Server mengembalikan HTTP ${response.statusCode}. '
                'URL: $uri. Kemungkinan request masuk ke virtual host/proxy yang salah. '
                'Detail awal respons: $bodyPreview',
        statusCode: response.statusCode,
      );
    }

    final Object decoded;
    try {
      decoded = response.body.isEmpty
          ? <String, dynamic>{}
          : jsonDecode(response.body);
    } catch (error, stackTrace) {
      debugPrint(
        'MyLIS API decode gagal: ${response.request?.method ?? '-'} $uri '
        'HTTP ${response.statusCode}; error=$error',
      );
      debugPrintStack(stackTrace: stackTrace);
      throw ApiException(
        'Respons server tidak bisa dibaca. '
        'URL: $uri. Status HTTP: ${response.statusCode}.',
        statusCode: response.statusCode,
      );
    }

    if (response.statusCode >= 200 && response.statusCode < 300) {
      final cleanDecoded = _decodeHtmlEntitiesIn(decoded);
      return cleanDecoded is Map<String, dynamic>
          ? cleanDecoded
          : {'data': cleanDecoded};
    }
    return {'data': _decodeHtmlEntitiesIn(decoded)};
  }

  bool _looksLikeJson(http.Response response) {
    final contentType = response.headers['content-type']?.toLowerCase() ?? '';
    final body = response.body.trimLeft();
    return contentType.contains('json') ||
        body.startsWith('{') ||
        body.startsWith('[');
  }

  ApiException _connectionException(
    Uri uri,
    Object error,
    StackTrace stackTrace,
  ) {
    if (error is ApiException) {
      return error;
    }

    final reason = _connectionReason(error);
    debugPrint('MyLIS koneksi gagal: $uri; $reason; error=$error');
    debugPrintStack(stackTrace: stackTrace);
    return ApiException('Koneksi server gagal. URL: $uri. Penyebab: $reason');
  }

  String _connectionReason(Object error) {
    if (error is TimeoutException) {
      return 'timeout lebih dari 15 detik. Periksa jaringan, DNS lokal, atau server Laravel belum aktif.';
    }
    if (error is http.ClientException) {
      final message = error.message.toLowerCase();
      if (message.contains('certificate') ||
          message.contains('handshake') ||
          message.contains('tls')) {
        return 'sertifikat HTTPS lokal belum dipercaya oleh device/simulator.';
      }
      if (message.contains('failed host lookup') ||
          message.contains('nodename') ||
          message.contains('name or service')) {
        return 'host tidak ditemukan. Periksa URL atau DNS/hosts lokal.';
      }
      if (message.contains('connection refused')) {
        return 'server menolak koneksi. Periksa apakah Laravel/web server aktif.';
      }
      if (message.contains('connection closed')) {
        return 'koneksi ditutup oleh server.';
      }
      return 'client HTTP gagal: ${error.message}';
    }
    if (error is FormatException) {
      return 'format URL atau respons tidak valid: ${error.message}';
    }
    return error.toString();
  }

  Object? _decodeHtmlEntitiesIn(Object? value) {
    if (value is String) {
      return decodeHtmlEntities(value);
    }
    if (value is List) {
      return value.map(_decodeHtmlEntitiesIn).toList();
    }
    if (value is Map) {
      return value.map(
        (key, item) => MapEntry(key.toString(), _decodeHtmlEntitiesIn(item)),
      );
    }
    return value;
  }
}

class ApiException implements Exception {
  ApiException(this.message, {this.statusCode});
  final String message;
  final int? statusCode;
}
