using AutoMapper; using Microsoft.Extensions.Logging; using PowderCoating.Application.DTOs.Company; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Core.Interfaces; namespace PowderCoating.Infrastructure.Services; /// /// Manages the community formula library: sharing a company template to the platform-wide /// library, removing it, browsing published entries, and importing an entry as a local copy. /// public class FormulaLibraryService : IFormulaLibraryService { private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; private readonly ILogger _logger; public FormulaLibraryService(IUnitOfWork unitOfWork, IMapper mapper, ILogger logger) { _unitOfWork = unitOfWork; _mapper = mapper; _logger = logger; } /// public async Task> BrowseAsync( int companyId, string? search, string? outputMode, string? industryHint) { var items = await _unitOfWork.FormulaLibrary.FindAsync(i => i.IsPublished); if (!string.IsNullOrWhiteSpace(search)) { var lower = search.ToLowerInvariant(); items = items.Where(i => i.Name.ToLowerInvariant().Contains(lower) || (i.Description != null && i.Description.ToLowerInvariant().Contains(lower)) || (i.Tags != null && i.Tags.ToLowerInvariant().Contains(lower)) || i.SourceCompanyName.ToLowerInvariant().Contains(lower)); } if (!string.IsNullOrWhiteSpace(outputMode)) items = items.Where(i => i.OutputMode == outputMode); if (!string.IsNullOrWhiteSpace(industryHint)) items = items.Where(i => i.IndustryHint != null && i.IndustryHint.ToLowerInvariant().Contains(industryHint.ToLowerInvariant())); // Load InspiredBy for attribution line var itemList = items.ToList(); var inspiredByIds = itemList .Where(i => i.InspiredByFormulaLibraryItemId.HasValue) .Select(i => i.InspiredByFormulaLibraryItemId!.Value) .Distinct() .ToHashSet(); Dictionary inspirations = new(); foreach (var id in inspiredByIds) { var parent = await _unitOfWork.FormulaLibrary.GetByIdAsync(id); if (parent != null) inspirations[id] = parent; } // Attach navigation properties manually (PlainRepository doesn't eager-load) foreach (var item in itemList) { if (item.InspiredByFormulaLibraryItemId.HasValue && inspirations.TryGetValue(item.InspiredByFormulaLibraryItemId.Value, out var parent)) item.InspiredBy = parent; } // Determine which entries this company has already imported var imports = await _unitOfWork.FormulaLibraryImports.FindAsync( imp => imp.CompanyId == companyId && !imp.IsDeleted); var importedIds = imports.Select(imp => imp.FormulaLibraryItemId).ToHashSet(); // Load all ratings in one query for this page of items var allItemIds = itemList.Select(i => i.Id).ToHashSet(); var allRatings = await _unitOfWork.FormulaLibraryRatings.FindAsync( r => allItemIds.Contains(r.FormulaLibraryItemId)); // Group counts and find the current company's vote per item var ratingsByItem = allRatings .GroupBy(r => r.FormulaLibraryItemId) .ToDictionary(g => g.Key, g => g.ToList()); var dtos = _mapper.Map>(itemList); for (int i = 0; i < dtos.Count; i++) { dtos[i].AlreadyImported = importedIds.Contains(dtos[i].Id); dtos[i].IsOwnFormula = itemList[i].SourceCompanyId == companyId; if (ratingsByItem.TryGetValue(dtos[i].Id, out var ratings)) { dtos[i].ThumbsUp = ratings.Count(r => r.IsPositive); dtos[i].ThumbsDown = ratings.Count(r => !r.IsPositive); var myRating = ratings.FirstOrDefault(r => r.CompanyId == companyId); dtos[i].MyVote = myRating?.IsPositive; } } // Sort: thumbs-up score descending, then import count, then name return dtos .OrderByDescending(d => d.ThumbsUp - d.ThumbsDown) .ThenByDescending(d => d.ImportCount) .ThenBy(d => d.Name); } /// public async Task GetDetailAsync(int libraryItemId, int companyId) { var item = await _unitOfWork.FormulaLibrary.GetByIdAsync(libraryItemId); if (item == null || !item.IsPublished) return null; if (item.InspiredByFormulaLibraryItemId.HasValue) item.InspiredBy = await _unitOfWork.FormulaLibrary.GetByIdAsync( item.InspiredByFormulaLibraryItemId.Value); var dto = _mapper.Map(item); var imp = await _unitOfWork.FormulaLibraryImports.FindAsync( i => i.CompanyId == companyId && i.FormulaLibraryItemId == libraryItemId && !i.IsDeleted); dto.AlreadyImported = imp.Any(); return dto; } /// public async Task ShareAsync(int companyId, string userId, ShareFormulaRequest request) { var template = await _unitOfWork.CustomItemTemplates.GetByIdAsync(request.CustomItemTemplateId); if (template == null || template.CompanyId != companyId) throw new InvalidOperationException("Template not found."); if (!CanShare(template)) throw new InvalidOperationException("This template is not eligible to be shared."); // Determine "Inspired by" — if this was imported from the library int? inspiredById = null; if (template.SourceFormulaLibraryItemId.HasValue && template.IsModifiedFromSource) inspiredById = template.SourceFormulaLibraryItemId; // Get company name for attribution var company = await _unitOfWork.Companies.GetByIdAsync(companyId); var companyName = company?.CompanyName ?? "Unknown Company"; // Re-use existing row if one exists (re-share after unshare, or update after edits) var existing = await _unitOfWork.FormulaLibrary.FirstOrDefaultAsync( f => f.SourceCustomItemTemplateId == template.Id && f.SourceCompanyId == companyId); if (existing != null) { CopyFromTemplate(existing, template, companyName, request); existing.InspiredByFormulaLibraryItemId = inspiredById; existing.IsPublished = true; existing.UpdatedAt = DateTime.UtcNow; await _unitOfWork.FormulaLibrary.UpdateAsync(existing); await _unitOfWork.CompleteAsync(); return existing.Id; } var libraryItem = new FormulaLibraryItem { SharedByUserId = userId, SharedAt = DateTime.UtcNow, SourceCustomItemTemplateId = template.Id, SourceCompanyId = companyId, SourceCompanyName = companyName, InspiredByFormulaLibraryItemId = inspiredById, IsPublished = true, }; CopyFromTemplate(libraryItem, template, companyName, request); await _unitOfWork.FormulaLibrary.AddAsync(libraryItem); await _unitOfWork.CompleteAsync(); return libraryItem.Id; } /// public async Task UnshareAsync(int libraryItemId, int companyId) { var item = await _unitOfWork.FormulaLibrary.GetByIdAsync(libraryItemId); if (item == null || item.SourceCompanyId != companyId) return; item.IsPublished = false; item.UpdatedAt = DateTime.UtcNow; await _unitOfWork.FormulaLibrary.UpdateAsync(item); await _unitOfWork.CompleteAsync(); } /// public async Task ImportAsync(int libraryItemId, int companyId, string userId) { var libraryItem = await _unitOfWork.FormulaLibrary.GetByIdAsync(libraryItemId); if (libraryItem == null || !libraryItem.IsPublished) throw new InvalidOperationException("Library entry not found or no longer published."); // Return existing import if already imported var existingImports = await _unitOfWork.FormulaLibraryImports.FindAsync( i => i.CompanyId == companyId && i.FormulaLibraryItemId == libraryItemId && !i.IsDeleted); var existingImport = existingImports.FirstOrDefault(); if (existingImport != null) return existingImport.ResultingCustomItemTemplateId; // Create a local copy as a new CustomItemTemplate var template = new CustomItemTemplate { CompanyId = companyId, Name = libraryItem.Name, Description = libraryItem.Description, OutputMode = libraryItem.OutputMode, FieldsJson = libraryItem.FieldsJson, Formula = libraryItem.Formula, DefaultRate = libraryItem.DefaultRate, RateLabel = libraryItem.RateLabel, Notes = libraryItem.Notes, DiagramImagePath = libraryItem.DiagramImagePath, DisplayOrder = 0, IsActive = true, SourceFormulaLibraryItemId = libraryItemId, IsModifiedFromSource = false, }; await _unitOfWork.CustomItemTemplates.AddAsync(template); await _unitOfWork.CompleteAsync(); var importRecord = new FormulaLibraryImport { CompanyId = companyId, FormulaLibraryItemId = libraryItemId, ImportedByUserId = userId, ImportedAt = DateTime.UtcNow, ResultingCustomItemTemplateId = template.Id, }; await _unitOfWork.FormulaLibraryImports.AddAsync(importRecord); // Increment import counter libraryItem.ImportCount++; await _unitOfWork.FormulaLibrary.UpdateAsync(libraryItem); await _unitOfWork.CompleteAsync(); return template.Id; } /// public async Task GetTemplateLibraryStatusAsync(int templateId, int companyId) { var template = await _unitOfWork.CustomItemTemplates.GetByIdAsync(templateId); if (template == null || template.CompanyId != companyId) return new FormulaLibraryStatusDto { CanShare = false }; var dto = new FormulaLibraryStatusDto { CanShare = CanShare(template) }; // Populate import attribution if (template.SourceFormulaLibraryItemId.HasValue) { var source = await _unitOfWork.FormulaLibrary.GetByIdAsync(template.SourceFormulaLibraryItemId.Value); if (source != null) { dto.ImportedFromName = source.Name; dto.ImportedFromCompany = source.SourceCompanyName; } } // Check if this template has an active library entry var libraryItem = await _unitOfWork.FormulaLibrary.FirstOrDefaultAsync( f => f.SourceCustomItemTemplateId == templateId && f.SourceCompanyId == companyId); if (libraryItem != null) { dto.LibraryItemId = libraryItem.Id; dto.IsPublished = libraryItem.IsPublished; } return dto; } /// public async Task CascadeRemoveDiagramAsync(int sourceCustomItemTemplateId) { // Null out on the library item published from this template var libraryItem = await _unitOfWork.FormulaLibrary.FirstOrDefaultAsync( f => f.SourceCustomItemTemplateId == sourceCustomItemTemplateId); if (libraryItem != null && libraryItem.DiagramImagePath != null) { libraryItem.DiagramImagePath = null; libraryItem.UpdatedAt = DateTime.UtcNow; await _unitOfWork.FormulaLibrary.UpdateAsync(libraryItem); // Null out on all imported copies var imports = await _unitOfWork.FormulaLibraryImports.FindAsync( i => i.FormulaLibraryItemId == libraryItem.Id && !i.IsDeleted); foreach (var imp in imports) { var copy = await _unitOfWork.CustomItemTemplates.GetByIdAsync( imp.ResultingCustomItemTemplateId); if (copy != null && copy.DiagramImagePath != null) { copy.DiagramImagePath = null; await _unitOfWork.CompleteAsync(); } } } } /// public async Task<(int ThumbsUp, int ThumbsDown, bool? MyVote)> RateAsync( int libraryItemId, int companyId, bool isPositive) { var item = await _unitOfWork.FormulaLibrary.GetByIdAsync(libraryItemId); if (item == null || !item.IsPublished) throw new InvalidOperationException("Library entry not found."); // Companies cannot rate their own formula if (item.SourceCompanyId == companyId) throw new InvalidOperationException("You cannot rate your own formula."); var existing = await _unitOfWork.FormulaLibraryRatings.FirstOrDefaultAsync( r => r.FormulaLibraryItemId == libraryItemId && r.CompanyId == companyId); bool? myVote; if (existing != null && existing.IsPositive == isPositive) { // Same vote again — toggle off await _unitOfWork.FormulaLibraryRatings.DeleteAsync(existing); myVote = null; } else if (existing != null) { // Opposite vote — flip it existing.IsPositive = isPositive; existing.RatedAt = DateTime.UtcNow; await _unitOfWork.FormulaLibraryRatings.UpdateAsync(existing); myVote = isPositive; } else { // New vote await _unitOfWork.FormulaLibraryRatings.AddAsync(new FormulaLibraryRating { FormulaLibraryItemId = libraryItemId, CompanyId = companyId, IsPositive = isPositive, RatedAt = DateTime.UtcNow, }); myVote = isPositive; } await _unitOfWork.CompleteAsync(); // Return fresh counts var allRatings = await _unitOfWork.FormulaLibraryRatings.FindAsync( r => r.FormulaLibraryItemId == libraryItemId); var list = allRatings.ToList(); return (list.Count(r => r.IsPositive), list.Count(r => !r.IsPositive), myVote); } // ── Helpers ─────────────────────────────────────────────────────────── /// /// A template is shareable if it was created fresh (no source library item) or /// if it was imported but then modified by the company. /// private static bool CanShare(CustomItemTemplate t) => t.SourceFormulaLibraryItemId == null || t.IsModifiedFromSource; private static void CopyFromTemplate( FormulaLibraryItem dest, CustomItemTemplate src, string companyName, ShareFormulaRequest req) { dest.Name = src.Name; dest.Description = src.Description; dest.OutputMode = src.OutputMode; dest.FieldsJson = src.FieldsJson; dest.Formula = src.Formula; dest.DefaultRate = src.DefaultRate; dest.RateLabel = src.RateLabel; dest.Notes = src.Notes; dest.DiagramImagePath = src.DiagramImagePath; dest.SourceCompanyName = companyName; dest.Tags = req.Tags?.Trim(); dest.IndustryHint = req.IndustryHint?.Trim(); } }