using Microsoft.EntityFrameworkCore; using System.Globalization; using System.Text; using LittleShop.Data; using LittleShop.Models; using LittleShop.DTOs; using LittleShop.Enums; namespace LittleShop.Services; public interface IProductImportService { Task ImportFromCsvAsync(Stream csvStream); Task ImportFromTextAsync(string csvText); string GenerateTemplateAsCsv(); Task ExportProductsAsCsvAsync(); } public class ProductImportService : IProductImportService { private readonly LittleShopContext _context; private readonly IProductService _productService; private readonly ICategoryService _categoryService; private readonly ILogger _logger; public ProductImportService( LittleShopContext context, IProductService productService, ICategoryService categoryService, ILogger logger) { _context = context; _productService = productService; _categoryService = categoryService; _logger = logger; } public async Task ImportFromCsvAsync(Stream csvStream) { using var reader = new StreamReader(csvStream); var csvText = await reader.ReadToEndAsync(); return await ImportFromTextAsync(csvText); } public async Task ImportFromTextAsync(string csvText) { var result = new ProductImportResultDto(); var lines = csvText.Split('\n', StringSplitOptions.RemoveEmptyEntries); if (lines.Length == 0) { result.Errors.Add(new ProductImportErrorDto { RowNumber = 0, ProductName = "File", ErrorMessages = { "CSV file is empty" } }); return result; } // Parse header var headers = ParseCsvLine(lines[0]); var expectedHeaders = new[] { "Name", "Description", "Price", "Weight", "WeightUnit", "StockQuantity", "CategoryName", "IsActive", "Variations", "PhotoUrls" }; // Validate headers foreach (var expectedHeader in expectedHeaders.Take(7)) // First 7 are required { if (!headers.Contains(expectedHeader, StringComparer.OrdinalIgnoreCase)) { result.Errors.Add(new ProductImportErrorDto { RowNumber = 0, ProductName = "Header", ErrorMessages = { $"Missing required column: {expectedHeader}" } }); } } if (result.Errors.Any()) return result; result.TotalRows = lines.Length - 1; // Exclude header // Get categories for lookup var categories = await _categoryService.GetAllCategoriesAsync(); var categoryLookup = categories.ToDictionary(c => c.Name, c => c.Id, StringComparer.OrdinalIgnoreCase); // Process data rows for (int i = 1; i < lines.Length; i++) { try { var values = ParseCsvLine(lines[i]); if (values.Length < 7) // Minimum required columns { result.Errors.Add(new ProductImportErrorDto { RowNumber = i + 1, ProductName = values.Length > 0 ? values[0] : "Unknown", ErrorMessages = { "Insufficient columns in row" } }); result.FailedImports++; continue; } var importDto = new ProductImportDto { Name = GetValue(values, headers, "Name", ""), Description = GetValue(values, headers, "Description", ""), Price = decimal.Parse(GetValue(values, headers, "Price", "0"), CultureInfo.InvariantCulture), Weight = decimal.Parse(GetValue(values, headers, "Weight", "0"), CultureInfo.InvariantCulture), WeightUnit = GetValue(values, headers, "WeightUnit", "Grams"), StockQuantity = int.Parse(GetValue(values, headers, "StockQuantity", "0")), CategoryName = GetValue(values, headers, "CategoryName", ""), IsActive = bool.Parse(GetValue(values, headers, "IsActive", "true")), Variations = GetValue(values, headers, "Variations", null), PhotoUrls = GetValue(values, headers, "PhotoUrls", null) }; await ImportSingleProductAsync(importDto, categoryLookup, result, i + 1); } catch (Exception ex) { result.Errors.Add(new ProductImportErrorDto { RowNumber = i + 1, ProductName = "Parse Error", ErrorMessages = { ex.Message } }); result.FailedImports++; } } _logger.LogInformation("Product import completed: {Success} successful, {Failed} failed", result.SuccessfulImports, result.FailedImports); return result; } private async Task ImportSingleProductAsync(ProductImportDto importDto, Dictionary categoryLookup, ProductImportResultDto result, int rowNumber) { var errors = new List(); // Validate category if (!categoryLookup.TryGetValue(importDto.CategoryName, out var categoryId)) { errors.Add($"Category '{importDto.CategoryName}' not found"); } // Validate weight unit if (!Enum.TryParse(importDto.WeightUnit, true, out var weightUnit)) { errors.Add($"Invalid weight unit: {importDto.WeightUnit}"); } if (errors.Any()) { result.Errors.Add(new ProductImportErrorDto { RowNumber = rowNumber, ProductName = importDto.Name, ErrorMessages = errors }); result.FailedImports++; return; } try { // Create product var createProductDto = new CreateProductDto { Name = importDto.Name, Description = importDto.Description, Price = importDto.Price, Weight = importDto.Weight, WeightUnit = weightUnit, StockQuantity = importDto.StockQuantity, CategoryId = categoryId }; var product = await _productService.CreateProductAsync(createProductDto); result.ImportedProducts.Add(product); // Import variations if provided if (!string.IsNullOrEmpty(importDto.Variations)) { await ImportProductMultiBuysAsync(product.Id, importDto.Variations); } // Import photos if provided if (!string.IsNullOrEmpty(importDto.PhotoUrls)) { await ImportProductPhotosAsync(product.Id, importDto.PhotoUrls); } result.SuccessfulImports++; } catch (Exception ex) { result.Errors.Add(new ProductImportErrorDto { RowNumber = rowNumber, ProductName = importDto.Name, ErrorMessages = { ex.Message } }); result.FailedImports++; } } private async Task ImportProductMultiBuysAsync(Guid productId, string variationsText) { // Format: "Single Item:1:10.00;Twin Pack:2:19.00;Triple Pack:3:25.00" var variations = variationsText.Split(';', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < variations.Length; i++) { var parts = variations[i].Split(':', StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 3) { var multiBuyDto = new CreateProductMultiBuyDto { ProductId = productId, Name = parts[0].Trim(), Description = parts.Length > 3 ? parts[3].Trim() : "", Quantity = int.Parse(parts[1].Trim()), Price = decimal.Parse(parts[2].Trim(), CultureInfo.InvariantCulture), SortOrder = i }; await _productService.CreateProductMultiBuyAsync(multiBuyDto); } } } private async Task ImportProductPhotosAsync(Guid productId, string photoUrlsText) { // Format: "url1;url2;url3" var urls = photoUrlsText.Split(';', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < urls.Length; i++) { var photoDto = new CreateProductPhotoDto { ProductId = productId, PhotoUrl = urls[i].Trim(), DisplayOrder = i + 1 }; await _productService.AddProductPhotoAsync(photoDto); } } public string GenerateTemplateAsCsv() { var sb = new StringBuilder(); // Header sb.AppendLine("Name,Description,Price,Weight,WeightUnit,StockQuantity,CategoryName,IsActive,Variations,PhotoUrls"); // Example rows sb.AppendLine("\"Example Product 1\",\"High-quality example product with great features\",29.99,150,Grams,50,Electronics,true,\"Single Item:1:29.99;Twin Pack:2:55.00;Triple Pack:3:79.99\",\"https://example.com/photo1.jpg;https://example.com/photo2.jpg\""); sb.AppendLine("\"Example Product 2\",\"Another sample product for import testing\",19.99,200,Grams,25,Clothing,true,\"Individual:1:19.99;Pair:2:35.00\",\"\""); sb.AppendLine("\"Simple Product\",\"Basic product without variations\",9.99,100,Grams,100,Books,true,\"\",\"\""); return sb.ToString(); } public async Task ExportProductsAsCsvAsync() { var products = await _productService.GetAllProductsAsync(); var sb = new StringBuilder(); // Header sb.AppendLine("Name,Description,Price,Weight,WeightUnit,StockQuantity,CategoryName,IsActive,Variations,PhotoUrls"); foreach (var product in products) { // Build variations string var variationsText = string.Join(";", product.MultiBuys.OrderBy(v => v.SortOrder) .Select(v => $"{v.Name}:{v.Quantity}:{v.Price:F2}")); // Build photo URLs string var photoUrlsText = string.Join(";", product.Photos.OrderBy(p => p.SortOrder) .Select(p => p.FilePath)); sb.AppendLine($"\"{product.Name}\",\"{product.Description}\",{product.Price:F2},{product.Weight:F2},{product.WeightUnit},{product.StockQuantity},\"{product.CategoryName}\",{product.IsActive.ToString().ToLower()},\"{variationsText}\",\"{photoUrlsText}\""); } return sb.ToString(); } private string[] ParseCsvLine(string line) { var values = new List(); var inQuotes = false; var currentValue = new StringBuilder(); for (int i = 0; i < line.Length; i++) { var c = line[i]; if (c == '"') { inQuotes = !inQuotes; } else if (c == ',' && !inQuotes) { values.Add(currentValue.ToString().Trim()); currentValue.Clear(); } else { currentValue.Append(c); } } values.Add(currentValue.ToString().Trim()); return values.ToArray(); } private string GetValue(string[] values, string[] headers, string columnName, string defaultValue) { var index = Array.FindIndex(headers, h => h.Equals(columnName, StringComparison.OrdinalIgnoreCase)); if (index >= 0 && index < values.Length) { var value = values[index].Trim('"', ' '); return string.IsNullOrEmpty(value) ? defaultValue : value; } return defaultValue; } }