using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using CMSMicroservice.Protobuf.Protos.Products; using Google.Protobuf.WellKnownTypes; namespace FrontOffice.Main.Utilities; public record Product( long Id, string Title, string Description, string FullInformation, string ImageUrl, long Price, int Discount = 1, int Rate = 0, int RemainingCount = 0) { public IReadOnlyList Gallery { get; init; } = []; public IReadOnlyList Categories { get; init; } = []; } public record ProductListResult( List Products, int TotalCount, int TotalPages, int CurrentPage, bool HasNext); public record ProductGalleryImage( long ProductGalleryId, long ProductImageId, string Title, string ImageUrl, string ThumbnailUrl); public record ProductCategoryNodeInfo( long Id, string Title, long? ParentId); public record ProductCategoryPathInfo( long CategoryId, string Title, IReadOnlyList Nodes) { public string DisplayLabel => string.Join(" › ", Nodes.Select(node => node.Title)); public ProductCategoryNodeInfo Leaf => Nodes.Last(); } public class ProductService { private readonly ConcurrentDictionary _cache = new(); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(1); private readonly CMSMicroservice.Protobuf.Protos.Products.ProductsContract.ProductsContractClient _client; public ProductService(CMSMicroservice.Protobuf.Protos.Products.ProductsContract.ProductsContractClient client) { _client = client; } public async Task> GetProductsAsync(string? query = null, long? categoryId = null, string? sortBy = null) { var result = await GetProductsPagedAsync(query: query, categoryId: categoryId, sortBy: sortBy, page: 1, pageSize: 500); return result.Products; } public async Task GetProductsPagedAsync( string? query = null, long? categoryId = null, string? sortBy = null, int page = 1, int pageSize = 12) { try { var request = new GetAllProductsByFilterRequest { PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = page, PageSize = pageSize }, Filter = new GetAllProductsByFilterFilter { Title = query ?? string.Empty, Description = query ?? string.Empty, ShortInfomation = query ?? string.Empty, FullInformation = query ?? string.Empty } }; if (categoryId is { } value) { request.Filter.CategoryId = value; } if (!string.IsNullOrEmpty(sortBy)) { request.SortBy = sortBy; } var resp = await _client.GetAllProductsByFilterAsync(request); var products = MapAndCache(resp.Models); var totalCount = (int)(resp.MetaData?.TotalCount ?? 0); var totalPages = (int)(resp.MetaData?.TotalPage ?? 0); var hasNext = resp.MetaData?.HasNext ?? false; return new ProductListResult(products, totalCount, totalPages, page, hasNext); } catch { IEnumerable list = GetValidCachedProducts(); if (!string.IsNullOrWhiteSpace(query)) { var q = query.Trim(); list = list.Where(p => p.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || p.Description.Contains(q, StringComparison.OrdinalIgnoreCase)); } var all = list.OrderBy(p => p.Id).ToList(); return new ProductListResult(all, all.Count, 1, 1, false); } } public async Task GetByIdAsync(long id) { if (TryGetCachedProduct(id, out var cached) && HasDetailedData(cached)) { return cached; } try { var resp = await _client.GetProductsAsync(new GetProductsRequest { Id = id }); if (resp == null) { return null; } return MapAndCache(resp); } catch { if (cached is not null) { return cached; } TryGetCachedProduct(id, out var result); return result; } } private List MapAndCache( Google.Protobuf.Collections.RepeatedField models) { var list = new List(); foreach (var m in models) { var p = new Product( Id: m.Id, Title: m.Title ?? string.Empty, Description: m.Description ?? string.Empty, FullInformation: m.FullInformation ?? string.Empty, ImageUrl: m.ImagePath, Price: m.Price, Discount: m.Discount, Rate: m.Rate, RemainingCount: m.RemainingCount ); p = PreserveCachedDetails(p); CacheProduct(p); list.Add(p); } return list; } private Product PreserveCachedDetails(Product product) { if (_cache.TryGetValue(product.Id, out var entry)) { var cached = entry.Product; var hasGallery = cached.Gallery.Count > 0; var hasCategories = cached.Categories.Count > 0; if (hasGallery || hasCategories) { product = product with { Gallery = hasGallery ? cached.Gallery : product.Gallery, Categories = hasCategories ? cached.Categories : product.Categories }; } } return product; } private Product MapAndCache(GetProductsResponse model) { var gallery = model.Gallery .Select(item => new ProductGalleryImage( ProductGalleryId: item.ProductGalleryId, ProductImageId: item.ProductImageId, Title: item.Title ?? string.Empty, ImageUrl: BuildUrl(item.ImagePath), ThumbnailUrl: BuildUrl(item.ImageThumbnailPath))) .ToList(); var product = new Product( Id: model.Id, Title: model.Title ?? string.Empty, Description: model.Description ?? string.Empty, FullInformation: model.FullInformation ?? string.Empty, ImageUrl: BuildUrl(model.ImagePath), Price: model.Price, Discount: model.Discount, Rate: model.Rate, RemainingCount: model.RemainingCount) { Gallery = gallery, Categories = MapCategoryPaths(model.Categories) }; CacheProduct(product); return product; } private void CacheProduct(Product product) { var entry = new CacheEntry(product, DateTime.UtcNow.Add(CacheDuration)); _cache.AddOrUpdate(product.Id, entry, (_, _) => entry); } private bool TryGetCachedProduct(long id, [NotNullWhen(true)] out Product? product) { if (_cache.TryGetValue(id, out var entry)) { if (entry.Expiration > DateTime.UtcNow) { product = entry.Product; return true; } _cache.TryRemove(id, out _); } product = null; return false; } private IEnumerable GetValidCachedProducts() { var now = DateTime.UtcNow; return _cache.Values .Where(entry => entry.Expiration > now) .Select(entry => entry.Product); } private static bool HasDetailedData(Product product) => product.Gallery.Count > 0 || product.Categories.Count > 0; private sealed record CacheEntry(Product Product, DateTime Expiration); private static string BuildUrl(string? path) => path ?? string.Empty; private static IReadOnlyList MapCategoryPaths(IEnumerable? categories) { if (categories is null) { return Array.Empty(); } var result = new List(); foreach (var category in categories) { var nodes = category.Path .Select(node => new ProductCategoryNodeInfo( Id: node.Id, Title: node.Title ?? string.Empty, ParentId: node.ParentId )) .ToList(); if (nodes.Count == 0) { continue; } result.Add(new ProductCategoryPathInfo( CategoryId: category.CategoryId, Title: category.Title ?? string.Empty, Nodes: nodes)); } return result; } }