import 'dart:developer'; import 'dart:io'; import 'package:data_channel/data_channel.dart'; import 'package:injectable/injectable.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; import '../../../common/constant/app_constant.dart'; import '../../../common/database/database_helper.dart'; import '../../../domain/product/product.dart'; import '../product_dtos.dart'; @injectable class ProductLocalDataProvider { final DatabaseHelper _databaseHelper; final _logName = 'ProductLocalDataProvider'; final Map> _queryCache = {}; final Duration _cacheExpiry = Duration(minutes: AppConstant.cacheExpire); final Map _cacheTimestamps = {}; ProductLocalDataProvider(this._databaseHelper); Future> saveProductsBatch( List products, { bool clearFirst = false, }) async { final db = await _databaseHelper.database; try { await db.transaction((txn) async { if (clearFirst) { log('๐Ÿ—‘๏ธ Clearing existing products...', name: _logName); await txn.delete('product_variants'); await txn.delete('products'); } log('๐Ÿ’พ Batch saving ${products.length} products...', name: _logName); // โœ… Batch insert products final productBatch = txn.batch(); for (final product in products) { productBatch.insert( 'products', product.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } await productBatch.commit(noResult: true); // โœ… Batch insert variants final variantBatch = txn.batch(); for (final product in products) { if (product.variants?.isNotEmpty == true) { // Delete existing variants variantBatch.delete( 'product_variants', where: 'product_id = ?', whereArgs: [product.id], ); // Insert variants for (final variant in product.variants!) { variantBatch.insert( 'product_variants', variant.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } } } await variantBatch.commit(noResult: true); }); // โœ… Clear cache clearCache(); log( 'โœ… Successfully batch saved ${products.length} products', name: _logName, ); return DC.data(null); } catch (e, s) { log( 'โŒ Error batch saving products', name: _logName, error: e, stackTrace: s, ); return DC.error(ProductFailure.dynamicErrorMessage(e.toString())); } } Future>> getCachedProducts({ int page = 1, int limit = 10, String? categoryId, String? search, }) async { final cacheKey = _generateCacheKey(page, limit, categoryId, search); final now = DateTime.now(); try { // โœ… CHECK CACHE FIRST if (_queryCache.containsKey(cacheKey) && _cacheTimestamps.containsKey(cacheKey)) { final cacheTime = _cacheTimestamps[cacheKey]!; if (now.difference(cacheTime) < _cacheExpiry) { final cachedProducts = _queryCache[cacheKey]!; log( '๐Ÿš€ Cache HIT: $cacheKey (${cachedProducts.length} products)', name: _logName, ); return DC.data(cachedProducts); } } log('๐Ÿ“€ Cache MISS: $cacheKey, querying database...', name: _logName); // Cache miss โ†’ query database final result = await getProducts( page: page, limit: limit, categoryId: categoryId, search: search, ); // โœ… Handle data/error dari getProducts() if (result.hasData) { final products = result.data!; // Simpan ke cache _queryCache[cacheKey] = products; _cacheTimestamps[cacheKey] = now; log( '๐Ÿ’พ Cached ${products.length} products for key: $cacheKey', name: _logName, ); return DC.data(products); } else { // Kalau error dari getProducts return DC.error(result.error!); } } catch (e, s) { log( 'โŒ Error getting cached products', name: _logName, error: e, stackTrace: s, ); return DC.error(ProductFailure.localStorageError(e.toString())); } } Future>> getProducts({ int page = 1, int limit = 10, String? categoryId, String? search, }) async { final db = await _databaseHelper.database; try { String query = 'SELECT * FROM products WHERE 1=1'; List whereArgs = []; if (categoryId != null && categoryId.isNotEmpty) { query += ' AND category_id = ?'; whereArgs.add(categoryId); } if (search != null && search.isNotEmpty) { query += ' AND (name LIKE ? OR sku LIKE ? OR description LIKE ?)'; whereArgs.add('%$search%'); whereArgs.add('%$search%'); whereArgs.add('%$search%'); } query += ' ORDER BY created_at DESC'; if (limit > 0) { query += ' LIMIT ?'; whereArgs.add(limit); if (page > 1) { query += ' OFFSET ?'; whereArgs.add((page - 1) * limit); } } final List> maps = await db.rawQuery( query, whereArgs, ); final List products = []; for (final map in maps) { final variants = await _getProductVariants(db, map['id']); final product = ProductDto.fromMap(map, variants); products.add(product); } log( '๐Ÿ“Š Retrieved ${products.length} products from database', name: _logName, ); return DC.data(products); } catch (e, s) { log('โŒ Error getting products', name: _logName, error: e, stackTrace: s); return DC.error(ProductFailure.localStorageError(e.toString())); } } Future>> searchProductsOptimized( String query, ) async { final db = await _databaseHelper.database; try { log('๐Ÿ” Optimized search for: "$query"', name: _logName); // โœ… Smart query with prioritization final List> maps = await db.rawQuery( ''' SELECT * FROM products WHERE name LIKE ? OR sku LIKE ? OR description LIKE ? ORDER BY CASE WHEN name LIKE ? THEN 1 -- Highest priority: name match WHEN sku LIKE ? THEN 2 -- Second priority: SKU match ELSE 3 -- Lowest priority: description END, name ASC LIMIT 50 ''', [ '%$query%', '%$query%', '%$query%', '$query%', '$query%', // Prioritize results that start with query ], ); final List products = []; for (final map in maps) { final variants = await _getProductVariants(db, map['id']); final product = ProductDto.fromMap(map, variants); products.add(product); } log( '๐ŸŽฏ Optimized search found ${products.length} results', name: _logName, ); return DC.data(products); } catch (e, s) { log( 'โŒ Error in optimized search', name: _logName, error: e, stackTrace: s, ); return DC.error(ProductFailure.localStorageError(e.toString())); } } Future> getProductById(String id) async { final db = await _databaseHelper.database; try { final List> maps = await db.query( 'products', where: 'id = ?', whereArgs: [id], ); if (maps.isEmpty) { log('โŒ Product not found: $id', name: _logName); return DC.error(ProductFailure.empty()); } final variants = await _getProductVariants(db, id); final product = ProductDto.fromMap(maps.first, variants); log('โœ… Product found: ${product.name}', name: _logName); return DC.data(product); } catch (e, s) { log( 'โŒ Error getting product by ID', name: _logName, error: e, stackTrace: s, ); return DC.error(ProductFailure.localStorageError(e.toString())); } } Future>> getDatabaseStats() async { final db = await _databaseHelper.database; try { final productCount = Sqflite.firstIntValue( await db.rawQuery('SELECT COUNT(*) FROM products'), ) ?? 0; final variantCount = Sqflite.firstIntValue( await db.rawQuery('SELECT COUNT(*) FROM product_variants'), ) ?? 0; final categoryCount = Sqflite.firstIntValue( await db.rawQuery( 'SELECT COUNT(DISTINCT category_id) FROM products WHERE category_id IS NOT NULL', ), ) ?? 0; final dbSize = await _getDatabaseSize(); final stats = { 'total_products': productCount, 'total_variants': variantCount, 'total_categories': categoryCount, 'database_size_mb': dbSize, 'cache_entries': _queryCache.length, 'cache_size_mb': _getCacheSize(), }; log('๐Ÿ“Š Database Stats: $stats', name: _logName); return DC.data(stats); } catch (e, s) { log( 'โŒ Error getting database stats', name: _logName, error: e, stackTrace: s, ); return DC.error(ProductFailure.localStorageError(e.toString())); } } Future getTotalCount({String? categoryId, String? search}) async { final db = await _databaseHelper.database; try { String query = 'SELECT COUNT(*) FROM products WHERE 1=1'; List whereArgs = []; if (categoryId != null && categoryId.isNotEmpty) { query += ' AND category_id = ?'; whereArgs.add(categoryId); } if (search != null && search.isNotEmpty) { query += ' AND (name LIKE ? OR sku LIKE ? OR description LIKE ?)'; whereArgs.add('%$search%'); whereArgs.add('%$search%'); whereArgs.add('%$search%'); } final result = await db.rawQuery(query, whereArgs); final count = Sqflite.firstIntValue(result) ?? 0; log( '๐Ÿ“Š Total count: $count (categoryId: $categoryId, search: $search)', name: _logName, ); return count; } catch (e) { log('โŒ Error getting total count: $e', name: _logName); return 0; } } Future hasProducts() async { final count = await getTotalCount(); final hasData = count > 0; log('๐Ÿ” Has products: $hasData ($count products)', name: _logName); return hasData; } Future clearAllProducts() async { final db = await _databaseHelper.database; try { await db.transaction((txn) async { await txn.delete('product_variants'); await txn.delete('products'); }); clearCache(); log('๐Ÿ—‘๏ธ All products cleared from local DB', name: _logName); } catch (e) { log('โŒ Error clearing products: $e', name: _logName); rethrow; } } Future _getDatabaseSize() async { try { final dbPath = p.join(await getDatabasesPath(), 'db_pos.db'); final file = File(dbPath); if (await file.exists()) { final size = await file.length(); return size / (1024 * 1024); // Convert to MB } } catch (e) { log('Error getting database size: $e', name: _logName); } return 0.0; } Future> _getProductVariants( Database db, String productId, ) async { try { final List> maps = await db.query( 'product_variants', where: 'product_id = ?', whereArgs: [productId], orderBy: 'name ASC', ); return maps.map((map) => ProductVariantDto.fromMap(map)).toList(); } catch (e) { log( 'โŒ Error getting variants for product $productId: $e', name: _logName, ); return []; } } String _generateCacheKey( int page, int limit, String? categoryId, String? search, ) { return 'products_${page}_${limit}_${categoryId ?? 'null'}_${search ?? 'null'}'; } double _getCacheSize() { double totalSize = 0; _queryCache.forEach((key, products) { totalSize += products.length * 0.001; // Rough estimate in MB }); return totalSize; } void clearCache() { final count = _queryCache.length; _queryCache.clear(); _cacheTimestamps.clear(); log('๐Ÿงน Cache cleared: $count entries removed', name: _logName); } void clearExpiredCache() { final now = DateTime.now(); final expiredKeys = []; _cacheTimestamps.forEach((key, timestamp) { if (now.difference(timestamp) > _cacheExpiry) { expiredKeys.add(key); } }); for (final key in expiredKeys) { _queryCache.remove(key); _cacheTimestamps.remove(key); } if (expiredKeys.isNotEmpty) { log('โฐ Expired cache cleared: ${expiredKeys.length} entries'); } } }