using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using eprintServer.Data; using eprintServer.Entities; using eprintServer.Requests; using eprintServer.Enums; using eprintServer.Responses; namespace eprintServer.Services; public class ProductService { private readonly DataContext _context; public ProductService(DataContext context) { _context = context; } // Include Category and its Name list for the transient CategoryName property private IQueryable GetProductsWithDependencies() { return _context.SellableProducts .Include(p => p.Image) .Include(p => p.Descriptions) .Include(p => p.Names) .Include(p => p.Category) .ThenInclude(c => c!.Name) .AsQueryable(); } public async Task> GetAllProductsAsync( string? search = null, long? categoryId = null, // UPDATED: Use CategoryId for filtering SellableProductStatusEnum? status = null) { var query = GetProductsWithDependencies(); if (!string.IsNullOrWhiteSpace(search)) { var lowerSearch = search.ToLower(); query = query.Where(p => p.ProductNameEN.ToLower().Contains(lowerSearch) || p.ProductNameAR.ToLower().Contains(lowerSearch)); } // UPDATED: Filter by the new CategoryId FK if (categoryId.HasValue) { query = query.Where(p => p.CategoryId == categoryId.Value); } if (status.HasValue) { query = query.Where(p => p.Status == status.Value); } query = query.OrderByDescending(p => p.CreatedAt); return await query.ToListAsync(); } /* public async Task> GetPagedProductsAsync( int page = 0, int size = 5, string? search = null, long? categoryId = null, // UPDATED: Use CategoryId for filtering SellableProductStatusEnum? status = null) { if (page < 0) page = 0; if (size <= 0) size = 5; var query = GetProductsWithDependencies(); if (!string.IsNullOrWhiteSpace(search)) { var lowerSearch = search.ToLower(); query = query.Where(p => p.ProductNameEN.ToLower().Contains(lowerSearch) || p.ProductNameAR.ToLower().Contains(lowerSearch)); } // UPDATED: Filter by the new CategoryId FK if (categoryId.HasValue) { query = query.Where(p => p.CategoryId == categoryId.Value); } if (status.HasValue) { query = query.Where(p => p.Status == status.Value); } var totalCount = await query.CountAsync(); query = query.OrderByDescending(p => p.CreatedAt); var skip = page * size; var pagedItems = await query .Skip(skip) .Take(size) .ToListAsync(); return new PagedResult(pagedItems, totalCount, page, size); } */ public async Task> GetPagedProductsAsync( int page = 0, int size = 5, string? search = null, long? categoryId = null, SellableProductStatusEnum? status = null, decimal? lowPrice = null, // New input decimal? highPrice = null) // New input { // Ensure valid pagination bounds if (page < 0) page = 0; if (size <= 0) size = 5; var query = GetProductsWithDependencies(); // 1. Search Logic: Search across the Names list (DynamicValue collection) if (!string.IsNullOrWhiteSpace(search)) { var lowerSearch = search.ToLower(); // Removed ProductNameEN/AR check; pointing to Names collection query = query.Where(p => p.Names.Any(n => n.Value.ToLower().Contains(lowerSearch))); } // 2. Category Filtering if (categoryId.HasValue) { query = query.Where(p => p.CategoryId == categoryId.Value); } // 3. Status Filtering if (status.HasValue) { query = query.Where(p => p.Status == status.Value); } // 4. Price Filtering (Low/High) if (lowPrice.HasValue) { query = query.Where(p => (decimal)p.Price > lowPrice.Value); } if (highPrice.HasValue) { query = query.Where(p => (decimal)p.Price < highPrice.Value); } // Calculate count before pagination var totalCount = await query.CountAsync(); // Sorting and Pagination var pagedItems = await query .OrderByDescending(p => p.CreatedAt) .Skip(page * size) .Take(size) .ToListAsync(); return new PagedResult(pagedItems, totalCount, page, size); } public async Task GetProductByIdAsync(int productId) { return await GetProductsWithDependencies().FirstOrDefaultAsync(p => p.Id == productId); } public async Task CreateProductAsync(CreateSellableProductRequest request) { // 1. Validate Category // Note: Include Products for an in-memory graph consistency check (optional, but good) var category = await _context.ProductCategories .Include(c => c.Products) .FirstOrDefaultAsync(c => c.Id == request.CategoryId); if (category == null) { // Throw KeyNotFoundException for the controller to catch and return 400 throw new KeyNotFoundException($"ProductCategory not found by ID {request.CategoryId}."); } // 2. Create Product var product = new SellableProduct { ProductNameEN = request.ProductNameEN, ProductNameAR = request.ProductNameAR, Price = request.Price, StockQuantity = request.StockQuantity, DescriptionAR = request.DescriptionAR, DescriptionEN = request.DescriptionEN, // REMOVED: request.Category Status = request.Status, // Set the foreign key CategoryId = request.CategoryId, Names = request.Names.Select(n => new DynamicValue { LanguageTag = n.LanguageTag, Value = n.Value }).ToList(), Descriptions = request.Descriptions.Select(d => new DynamicValue { LanguageTag = d.LanguageTag, Value = d.Value }).ToList() }; // 3. Add to category collection in memory (bi-directional link) category.Products.Add(product); _context.SellableProducts.Add(product); await _context.SaveChangesAsync(); // Return the product with all navigation properties loaded (including Category/CategoryName) return await GetProductByIdAsync(product.Id) ?? throw new InvalidOperationException("Failed to retrieve newly created product."); } // UPDATED: Handle CategoryId change and removed Category enum update public async Task UpdateProductAsync(int productId, UpdateSellableProductRequest request) { // Use GetProductsWithDependencies to ensure Category is loaded for relationship management var product = await GetProductsWithDependencies().FirstOrDefaultAsync(p => p.Id == productId); if (product == null) { return null; } bool changed = false; // Update string properties if (request.ProductNameEN != null) { product.ProductNameEN = request.ProductNameEN; changed = true; } if (request.ProductNameAR != null) { product.ProductNameAR = request.ProductNameAR; changed = true; } if (request.DescriptionAR != null) { product.DescriptionAR = request.DescriptionAR; changed = true; } if (request.DescriptionEN != null) { product.DescriptionEN = request.DescriptionEN; changed = true; } // Conditional updates for nullable value types if (request.Price.HasValue) { product.Price = request.Price.Value; changed = true; } if (request.StockQuantity.HasValue) { product.StockQuantity = request.StockQuantity.Value; changed = true; } // REMOVED: if (request.Category.HasValue) if (request.Status.HasValue) { product.Status = request.Status.Value; changed = true; } // Handle CategoryId change if (request.CategoryId.HasValue && request.CategoryId.Value != product.CategoryId) { // 1. Validate New Category var newCategoryId = request.CategoryId.Value; var newCategory = await _context.ProductCategories .FirstOrDefaultAsync(c => c.Id == newCategoryId); if (newCategory == null) { // Throw KeyNotFoundException for the controller to catch and return 400 throw new KeyNotFoundException($"ProductCategory not found by ID {newCategoryId}."); } // 2. Update FK on Product product.CategoryId = newCategoryId; // EF Core will handle the relationship update in the database. changed = true; } // Translation Logic (Same as ProductCategory) if (request.Names != null) { _context.DynamicValues.RemoveRange(product.Names); product.Names.Clear(); product.Names.AddRange(request.Names.Select(dv => new DynamicValue { LanguageTag = dv.LanguageTag, Value = dv.Value })); changed = true; } if (request.Descriptions != null) { _context.DynamicValues.RemoveRange(product.Descriptions); product.Descriptions.Clear(); product.Descriptions.AddRange(request.Descriptions.Select(dv => new DynamicValue { LanguageTag = dv.LanguageTag, Value = dv.Value })); changed = true; } if (changed) { product.UpdatedAt = DateTime.UtcNow; _context.SellableProducts.Update(product); await _context.SaveChangesAsync(); } // Return the product with all navigation properties loaded return await GetProductByIdAsync(productId); } /// /// Updates the image of a Sellable Product. /// public async Task UpdateProductImageAsync(int productId, FileModel fileModel) { var product = await GetProductsWithDependencies().FirstOrDefaultAsync(p => p.Id == productId); if (product == null) { return null; } product.SetImage(fileModel); product.UpdatedAt = DateTime.UtcNow; _context.SellableProducts.Update(product); await _context.SaveChangesAsync(); return product; } // UPDATED: Ensure proper removal from category list (though EF handles the FK, keeping consistency) public async Task DeleteProductAsync(int productId) { // Must include Category to handle the in-memory list removal (although not strictly needed for EF Core context) var product = await GetProductsWithDependencies().FirstOrDefaultAsync(p => p.Id == productId); if (product == null) { return false; } // EF Core will handle the database removal based on the FK relationship. _context.SellableProducts.Remove(product); await _context.SaveChangesAsync(); return true; } }