import "dart:convert"; import "package:http/http.dart" as http; import "../models/cart.dart"; import "../models/menu_item.dart"; import "../models/restaurant.dart"; import "../models/service_point.dart"; class ApiRawResponse { final int statusCode; final String rawBody; final Map? json; const ApiRawResponse({ required this.statusCode, required this.rawBody, required this.json, }); } class LoginResponse { final int userId; final String userFirstName; final String token; const LoginResponse({ required this.userId, required this.userFirstName, required this.token, }); factory LoginResponse.fromJson(Map json) { return LoginResponse( userId: (json["UserID"] as num).toInt(), userFirstName: (json["UserFirstName"] as String?) ?? "", token: (json["Token"] as String?) ?? "", ); } } class Api { static String? _userToken; // MVP hardcode static int _mvpBusinessId = 17; static void setAuthToken(String? token) => _userToken = token; static void setBusinessId(int? businessId) { if (businessId != null && businessId > 0) { _mvpBusinessId = businessId; } } static void clearCookies() { // no-op } static String get baseUrl { const v = String.fromEnvironment("AALISTS_API_BASE_URL"); if (v.isEmpty) { throw StateError( "AALISTS_API_BASE_URL is not set. Example (Android emulator): " "--dart-define=AALISTS_API_BASE_URL=http://10.0.2.2:8888/biz.payfrit.com/api", ); } return v; } static Uri _u(String path) { final b = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl; final p = path.startsWith("/") ? path : "/$path"; return Uri.parse("$b$p"); } static Map _headers({required bool json, int? businessIdOverride}) { final h = {}; if (json) h["Content-Type"] = "application/json; charset=utf-8"; final tok = _userToken; if (tok != null && tok.isNotEmpty) { h["X-User-Token"] = tok; } final int bid = (businessIdOverride != null && businessIdOverride > 0) ? businessIdOverride : _mvpBusinessId; h["X-Business-ID"] = bid.toString(); return h; } static Map? _tryDecodeJsonMap(String body) { try { final decoded = jsonDecode(body); if (decoded is Map) return decoded; } catch (_) {} return null; } static Future _getRaw(String path, {int? businessIdOverride}) async { final url = _u(path); final resp = await http.get(url, headers: _headers(json: false, businessIdOverride: businessIdOverride)); final body = resp.body; final j = _tryDecodeJsonMap(body); // ignore: avoid_print print("API GET => $url"); // ignore: avoid_print print("STATUS => ${resp.statusCode}"); // ignore: avoid_print print("BODY => ${body.length > 2000 ? body.substring(0, 2000) : body}"); return ApiRawResponse(statusCode: resp.statusCode, rawBody: body, json: j); } static Future _postRaw( String path, Map payload, { int? businessIdOverride, }) async { final url = _u(path); final resp = await http.post( url, headers: _headers(json: true, businessIdOverride: businessIdOverride), body: jsonEncode(payload), ); final body = resp.body; final j = _tryDecodeJsonMap(body); // ignore: avoid_print print("API POST => $url"); // ignore: avoid_print print("BODY IN => ${jsonEncode(payload)}"); // ignore: avoid_print print("STATUS => ${resp.statusCode}"); // ignore: avoid_print print("BODY OUT => ${body.length > 2000 ? body.substring(0, 2000) : body}"); return ApiRawResponse(statusCode: resp.statusCode, rawBody: body, json: j); } static bool _ok(Map j) => j["OK"] == true || j["ok"] == true; static String _err(Map j) => (j["ERROR"] ?? j["error"] ?? "").toString(); static List? _pickArray(Map j, List keys) { for (final k in keys) { final v = j[k]; if (v is List) return v; } return null; } static Map _requireJson(ApiRawResponse raw, String label) { final j = raw.json; if (j == null) { throw StateError("$label request failed: ${raw.statusCode}\nNon-JSON response."); } return j; } // ------------------------- // Authentication // ------------------------- static Future login({ required String username, required String password, }) async { final raw = await _postRaw( "/auth/login.cfm", { "username": username, "password": password, }, ); final j = _requireJson(raw, "Login"); if (!_ok(j)) { final err = _err(j); if (err == "bad_credentials") { throw StateError("Invalid email/phone or password"); } else if (err == "missing_fields") { throw StateError("Username and password are required"); } else { throw StateError("Login failed: $err"); } } final response = LoginResponse.fromJson(j); // Store token for future requests setAuthToken(response.token); return response; } static void logout() { setAuthToken(null); clearCookies(); } // ------------------------- // Businesses (legacy model name: Restaurant) // ------------------------- static Future listRestaurantsRaw() async { return _getRaw("/businesses/list.cfm", businessIdOverride: _mvpBusinessId); } static Future> listRestaurants() async { final raw = await listRestaurantsRaw(); final j = _requireJson(raw, "Businesses"); if (!_ok(j)) { throw StateError( "Businesses API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["Businesses", "BUSINESSES"]); if (arr == null) { throw StateError("Businesses JSON missing Businesses array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(Restaurant.fromJson(e)); } else if (e is Map) { out.add(Restaurant.fromJson(e.cast())); } } return out; } // ------------------------- // Service Points // ------------------------- static Future> listServicePoints({required int businessId}) async { // CRITICAL: endpoint is behaving like it reads JSON body, not query/header. final raw = await _postRaw( "/servicepoints/list.cfm", {"BusinessID": businessId}, businessIdOverride: businessId, ); final j = _requireJson(raw, "ServicePoints"); if (!_ok(j)) { throw StateError( "ServicePoints API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["ServicePoints", "SERVICEPOINTS"]); if (arr == null) { throw StateError("ServicePoints JSON missing ServicePoints array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(ServicePoint.fromJson(e)); } else if (e is Map) { out.add(ServicePoint.fromJson(e.cast())); } } return out; } // ------------------------- // Menu Items // ------------------------- static Future> listMenuItems({required int businessId}) async { final raw = await _postRaw( "/menu/items.cfm", {"BusinessID": businessId}, businessIdOverride: businessId, ); final j = _requireJson(raw, "MenuItems"); if (!_ok(j)) { throw StateError( "MenuItems API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["Items", "ITEMS"]); if (arr == null) { throw StateError("MenuItems JSON missing Items array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(MenuItem.fromJson(e)); } else if (e is Map) { out.add(MenuItem.fromJson(e.cast())); } } return out; } // ------------------------- // Cart & Orders // ------------------------- static Future getOrCreateCart({ required int userId, required int businessId, required int servicePointId, required int orderTypeId, }) async { final raw = await _postRaw( "/orders/getOrCreateCart.cfm", { "OrderUserID": userId, "BusinessID": businessId, "OrderServicePointID": servicePointId, "OrderTypeID": orderTypeId, }, businessIdOverride: businessId, ); final j = _requireJson(raw, "GetOrCreateCart"); if (!_ok(j)) { throw StateError( "GetOrCreateCart API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } return Cart.fromJson(j); } static Future getCart({required int orderId}) async { final raw = await _postRaw( "/orders/getCart.cfm", {"OrderID": orderId}, ); final j = _requireJson(raw, "GetCart"); if (!_ok(j)) { throw StateError( "GetCart API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } return Cart.fromJson(j); } static Future setLineItem({ required int orderId, required int parentOrderLineItemId, required int itemId, required bool isSelected, int quantity = 1, String? remark, }) async { final raw = await _postRaw( "/orders/setLineItem.cfm", { "OrderID": orderId, "ParentOrderLineItemID": parentOrderLineItemId, "ItemID": itemId, "IsSelected": isSelected, "Quantity": quantity, if (remark != null && remark.isNotEmpty) "Remark": remark, }, ); final j = _requireJson(raw, "SetLineItem"); if (!_ok(j)) { throw StateError( "SetLineItem API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } return Cart.fromJson(j); } static Future submitOrder({required int orderId}) async { final raw = await _postRaw( "/orders/submit.cfm", {"OrderID": orderId}, ); final j = _requireJson(raw, "SubmitOrder"); if (!_ok(j)) { throw StateError( "SubmitOrder API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } } // ------------------------- // Beacons // ------------------------- static Future> listAllBeacons() async { final raw = await _getRaw("/beacons/list_all.cfm"); final j = _requireJson(raw, "ListAllBeacons"); if (!_ok(j)) { throw StateError( "ListAllBeacons API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["items", "ITEMS"]); if (arr == null) return {}; final Map uuidToBeaconId = {}; for (final e in arr) { if (e is! Map) continue; final item = e is Map ? e : e.cast(); final uuid = (item["BeaconUUID"] ?? item["BEACONUUID"] ?? "").toString().trim(); final beaconId = item["BeaconID"] ?? item["BEACONID"]; if (uuid.isNotEmpty && beaconId is num) { uuidToBeaconId[uuid] = beaconId.toInt(); } } return uuidToBeaconId; } static Future getBusinessFromBeacon({ required int beaconId, }) async { final raw = await _postRaw( "/beacons/getBusinessFromBeacon.cfm", {"BeaconID": beaconId}, ); final j = _requireJson(raw, "GetBusinessFromBeacon"); if (!_ok(j)) { throw StateError( "GetBusinessFromBeacon API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } final beacon = j["BEACON"] as Map? ?? {}; final business = j["BUSINESS"] as Map? ?? {}; final servicePoint = j["SERVICEPOINT"] as Map? ?? {}; return BeaconBusinessMapping( beaconId: _parseInt(beacon["BeaconID"]) ?? 0, beaconName: (beacon["BeaconName"] as String?) ?? "", businessId: _parseInt(business["BusinessID"]) ?? 0, businessName: (business["BusinessName"] as String?) ?? "", servicePointId: _parseInt(servicePoint["ServicePointID"]) ?? 0, servicePointName: (servicePoint["ServicePointName"] as String?) ?? "", ); } static int? _parseInt(dynamic value) { if (value == null) return null; if (value is int) return value; if (value is num) return value.toInt(); if (value is String) { if (value.isEmpty) return null; return int.tryParse(value); } return null; } } class BeaconBusinessMapping { final int beaconId; final String beaconName; final int businessId; final String businessName; final int servicePointId; final String servicePointName; const BeaconBusinessMapping({ required this.beaconId, required this.beaconName, required this.businessId, required this.businessName, required this.servicePointId, required this.servicePointName, }); }