import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/api/api_client.dart'; import '../../../core/api/api_paths.dart'; import '../../../core/api/guarded_api_call.dart'; import '../../auth/data/auth_providers.dart'; import '../domain/admin_ai_categorize_result.dart'; import '../domain/admin_category_node.dart'; import '../domain/admin_product.dart'; import '../domain/ai_model_info.dart'; import '../domain/pending_product.dart'; import '../domain/receipt_alias.dart'; import '../domain/user_admin.dart'; final adminRepositoryProvider = Provider((ref) { return AdminRepository(ref.watch(apiClientProvider), ref); }); class AdminRepository { final ApiClient _apiClient; final Ref _ref; AdminRepository(this._apiClient, this._ref); // ── Interna helpers ──────────────────────────────────────────────────────── Future _token() => _ref.read(authStateProvider.future); /// GET-anrop som returnerar en typad lista med [fromJson]. Future> _getList( String path, T Function(Map) fromJson, { bool requiresAuth = true, }) async { final token = requiresAuth ? await _token() : null; final data = await guardedApiCall( _ref, () => _apiClient.getJson(path, token: token), ); return _parseList(data, fromJson); } /// POST-anrop som returnerar ett typad objekt med [fromJson]. Future _post( String path, { required Map? body, required T Function(dynamic) parse, bool requiresAuth = true, }) async { final token = requiresAuth ? await _token() : null; final data = await guardedApiCall( _ref, () => _apiClient.postJson(path, body: body, token: token), ); return parse(data); } /// PATCH-anrop som returnerar ett typad objekt med [fromJson]. Future _patch( String path, { required Map body, required T Function(Map) parse, }) async { final token = await _token(); final data = await guardedApiCall( _ref, () => _apiClient.patchJson(path, body: body, token: token), ); return parse(Map.from(data as Map)); } /// Fire-and-forget PATCH. Future _patchVoid(String path, Map body) async { final token = await _token(); await guardedApiCall( _ref, () => _apiClient.patchJson(path, body: body, token: token), ); } /// Fire-and-forget POST. Future _postVoid(String path, [Map? body]) async { final token = await _token(); await guardedApiCall( _ref, () => _apiClient.postJson(path, body: body, token: token), ); } /// Fire-and-forget DELETE. Future _deleteVoid(String path) async { final token = await _token(); await guardedApiCall( _ref, () => _apiClient.deleteJson(path, token: token), ); } /// Tolerant listparsning — accepterar ren lista eller wrapper ({items, data}). static List _parseList( dynamic data, T Function(Map) fromJson, ) { final List raw; if (data is List) { raw = data; } else if (data is Map) { raw = (data['items'] as List?) ?? (data['data'] as List?) ?? const []; if (raw.isEmpty && data.isNotEmpty) { debugPrint('[AdminRepository] Unexpected API wrapper shape: ${data.keys}'); } } else { raw = const []; } return raw .whereType() .map((e) => fromJson(Map.from(e))) .toList(); } // ── Användare ────────────────────────────────────────────────────────────── Future> listUsers() => _getList(UserApiPaths.list, UserAdmin.fromJson); Future setRole(int userId, String newRole) => _patch(UserApiPaths.setRole(userId), body: {'role': newRole}, parse: UserAdmin.fromJson); Future setPremium(int userId, {required bool isPremium}) => _patch(UserApiPaths.setPremium(userId), body: {'isPremium': isPremium}, parse: UserAdmin.fromJson); Future setRecipeSharing(int userId, {required bool canShareRecipes}) => _patch(UserApiPaths.setRecipeSharing(userId), body: {'canShareRecipes': canShareRecipes}, parse: UserAdmin.fromJson); Future updateEmail(int userId, String email) => _patchVoid(UserApiPaths.updateEmail(userId), {'email': email}); Future createUser({ required String username, required String email, required String password, String role = 'user', }) => _post( UserApiPaths.list, body: { 'username': username, 'email': email, 'password': password, 'role': role, }, parse: (d) => UserAdmin.fromJson(d as Map), ); Future deleteUser(int userId) => _deleteVoid(UserApiPaths.delete(userId)); /// Returns `{ temporaryPassword, to, subject, body }`. Future> resetPassword(int userId) => _post>( UserApiPaths.resetPassword(userId), body: null, parse: (d) => d as Map, ); // ── Produkter ────────────────────────────────────────────────────────────── Future> listProducts() => _getList(ProductApiPaths.mine, AdminProduct.fromJson); Future> listDeletedProducts() => _getList(ProductApiPaths.deleted, AdminProduct.fromJson); Future> listPendingProducts() => _getList(ProductApiPaths.pending, PendingProduct.fromJson); Future setProductStatus(int productId, String status) => _patchVoid(ProductApiPaths.setStatus(productId), {'status': status}); Future setProductCategory(int productId, {required int? categoryId}) => _patchVoid(ProductApiPaths.update(productId), {'categoryId': categoryId}); Future removeProduct(int productId) => _deleteVoid(ProductApiPaths.remove(productId)); Future restoreProduct(int productId) => _postVoid(ProductApiPaths.restore(productId)); /// Skapar en ny aktiv produkt (kräver admin). Returnerar `{id, name, categoryId?}`. Future> createProduct(String name, {int? categoryId}) => _post>( ProductApiPaths.list, body: { 'name': name.trim(), if (categoryId != null) 'categoryId': categoryId, }, parse: (d) => d as Map, ); Future bulkSetCategory(List ids, {required int? categoryId}) => _postVoid(ProductApiPaths.bulkUpdate, {'ids': ids, 'categoryId': categoryId}); Future mergeProducts({ required int sourceProductId, required int targetProductId, }) => _postVoid(ProductApiPaths.merge, { 'sourceProductId': sourceProductId, 'targetProductId': targetProductId, }); Future> previewMerge({ required int sourceProductId, required int targetProductId, }) async { final token = await _token(); final data = await guardedApiCall( _ref, () => _apiClient.getJson( ProductApiPaths.mergePreview(sourceProductId, targetProductId), token: token, ), ); return Map.from(data as Map); } Future> aiCategorizeBulk({ List? productIds, }) async { final token = await _token(); final data = await guardedApiCall( _ref, () => _apiClient.postJson( ProductApiPaths.aiCategorizeBulk, body: (productIds == null || productIds.isEmpty) ? null : {'productIds': productIds}, token: token, ), ); return _parseList(data, AdminAiCategorizeResult.fromJson) .where((e) => e.productId > 0 && e.categoryId > 0) .toList(); } // ── Kategorier ───────────────────────────────────────────────────────────── Future> listCategoryTree() => _getList( CategoryApiPaths.tree, AdminCategoryNode.fromJson, requiresAuth: false, ); // ── AI-modeller ──────────────────────────────────────────────────────────── /// OBS: endpointen /ai/models kräver autentisering. Future> listAiModels() => _getList(AiApiPaths.models, AiModelInfo.fromJson); // ── Kvittoalias (admin/global fallback) ─────────────────────────────────── Future> listReceiptAliases() => _getList(ReceiptAliasApiPaths.list, ReceiptAlias.fromJson); Future upsertReceiptAlias({ required String receiptName, required int productId, bool isGlobal = false, }) => _postVoid(ReceiptAliasApiPaths.list, { 'receiptName': receiptName, 'productId': productId, 'isGlobal': isGlobal, }); Future removeReceiptAlias(int id) => _deleteVoid(ReceiptAliasApiPaths.remove(id)); }