import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; class ApiClient { ApiClient({http.Client? client}) : _client = client ?? http.Client(); static const baseUrl = String.fromEnvironment( 'SMARTPLUS_API_URL', defaultValue: 'https://demo.sistemax.net.pe', ); static const authBaseUrl = String.fromEnvironment( 'SMARTPLUS_AUTH_URL', defaultValue: baseUrl, ); static const tenantHost = String.fromEnvironment('SMARTPLUS_TENANT_HOST'); final http.Client _client; Future _effectiveBaseUrl({String? override}) async { if (override != null && override.isNotEmpty) return _normalizeBaseUrl(override); final prefs = await SharedPreferences.getInstance(); return _normalizeBaseUrl(prefs.getString('delivery_api_base_url') ?? baseUrl); } static String normalizeBaseUrl(String value) => _normalizeBaseUrl(value); static String _normalizeBaseUrl(String value) { final raw = value.trim().replaceFirst(RegExp(r'/+$'), ''); final uri = Uri.tryParse(raw); if (uri == null || uri.host.isEmpty) return raw; final isLocal = uri.host == '127.0.0.1' || uri.host == 'localhost' || uri.host.endsWith('.test'); if (!isLocal && uri.scheme == 'http') { return uri.replace(scheme: 'https').toString().replaceFirst(RegExp(r'/+$'), ''); } return raw; } Future> _headers({String? hostOverride}) async { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString('delivery_api_token'); final storedHost = prefs.getString('delivery_tenant_host'); return { 'Accept': 'application/json', 'Content-Type': 'application/json', if ((hostOverride ?? storedHost ?? tenantHost).isNotEmpty) 'Host': hostOverride ?? storedHost ?? tenantHost, if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token', }; } Future> get(String path) async { final response = await _client.get( Uri.parse('${await _effectiveBaseUrl()}$path'), headers: await _headers(), ); return _decode(response); } Future> post( String path, [ Map? body, String? idempotencyKey, ]) async { final headers = await _headers(); if (idempotencyKey != null && idempotencyKey.isNotEmpty) { headers['Idempotency-Key'] = idempotencyKey; } final response = await _client.post( Uri.parse('${await _effectiveBaseUrl()}$path'), headers: headers, body: jsonEncode(body ?? {}), ); return _decode(response); } Future> postForm( String path, Map fields, { String? baseUrlOverride, String? hostOverride, }) async { final headers = await _headers(hostOverride: hostOverride); headers.remove('Content-Type'); final response = await _client.post( Uri.parse('${await _effectiveBaseUrl(override: baseUrlOverride)}$path'), headers: headers, body: fields, ); return _decode(response); } Future> multipartPost( String path, { required Map fields, String? fileField, String? filePath, String? idempotencyKey, }) async { final headers = await _headers(); headers.remove('Content-Type'); if (idempotencyKey != null && idempotencyKey.isNotEmpty) { headers['Idempotency-Key'] = idempotencyKey; } final request = http.MultipartRequest( 'POST', Uri.parse('${await _effectiveBaseUrl()}$path'), ) ..headers.addAll(headers) ..fields.addAll(fields); if (fileField != null && filePath != null && filePath.isNotEmpty) { request.files.add(await http.MultipartFile.fromPath(fileField, filePath)); } // Usar el cliente inyectado mantiene timeouts, pruebas y manejo uniforme // de errores de red para cobros multipart. final response = await http.Response.fromStream( await _client.send(request), ); return _decode(response); } Map _decode(http.Response response) { final decoded = response.body.isEmpty ? {} : jsonDecode(response.body) as Map; if (response.statusCode < 200 || response.statusCode >= 300) { throw ApiException( response.statusCode, decoded['message']?.toString() ?? 'Error de servidor', ); } return decoded; } } class ApiException implements Exception { const ApiException(this.statusCode, this.message); final int statusCode; final String message; @override String toString() => 'ApiException($statusCode): $message'; }