using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; using System.Reflection; using SeaHaven.DataServices.Interfaces; namespace SeaHaven.DataServices.Implementation { public class VendorDataService : IVendorDataService { private readonly ApplicationDbContext _context; public VendorDataService(ApplicationDbContext context) { _context = context; } public async Task GetByIdAsync(int id) { return await _context.Vendors .Include(v => v.Dispatches!) .ThenInclude(dispatch => dispatch.DispatchWorkOrders) .FirstOrDefaultAsync(v => v.Id == id); } public async Task GetByIdWithDetailsAsync(int id) { return await _context.Vendors .Include(v => v.Dispatches!) .ThenInclude(dispatch => dispatch.DispatchWorkOrders) .FirstOrDefaultAsync(v => v.Id == id); } public async Task> GetAllAsync() { return await _context.Vendors.ToListAsync(); } public async Task<(IEnumerable Items, int TotalCount)> GetPagedAsync( int page, int pageSize, string? search = null, bool? isActive = true, IReadOnlyCollection? companies = null, IReadOnlyCollection? trades = null, IReadOnlyCollection? locations = null, IReadOnlyCollection? jobBuckets = null) { search ??= ""; var query = _context.Vendors.Where(v => v.IsDeleted == null || v.IsDeleted == false); if (isActive.HasValue) query = query.Where(v => v.IsActive == isActive.Value); if (!string.IsNullOrWhiteSpace(search)) { var s = search.ToLower(); query = query.Where(v => (v.CompanyName ?? "").ToLower().Contains(s) || (v.ContactName ?? "").ToLower().Contains(s) || (v.Email ?? "").ToLower().Contains(s) || (v.TradeSpecialties ?? "").ToLower().Contains(s) || (v.City ?? "").ToLower().Contains(s) || (v.State ?? "").ToLower().Contains(s) || (v.Zip ?? "").ToLower().Contains(s)); } var selectedCompanies = companies? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedCompanies.Length > 0) query = query.Where(v => selectedCompanies.Contains((v.CompanyName ?? "").ToLower())); var selectedTrades = trades? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedTrades.Length > 0) query = query.Where(BuildTradePredicate(selectedTrades)); var selectedLocations = locations? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedLocations.Length > 0) query = query.Where(BuildLocationPredicate(selectedLocations)); var selectedJobBuckets = jobBuckets? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedJobBuckets.Length > 0) { var under50 = selectedJobBuckets.Contains("under-50"); var from50To99 = selectedJobBuckets.Contains("50-99"); var from100To149 = selectedJobBuckets.Contains("100-149"); var from150 = selectedJobBuckets.Contains("150-plus"); var vendorsWithDispatches = await query .Include(v => v.Dispatches!) .ThenInclude(dispatch => dispatch.DispatchWorkOrders) .OrderByDescending(v => v.Id) .ToListAsync(); var filtered = vendorsWithDispatches.Where(vendor => { var totalJobs = CountDistinctWorkOrders(vendor); return (under50 && totalJobs < 50) || (from50To99 && totalJobs is >= 50 and <= 99) || (from100To149 && totalJobs is >= 100 and <= 149) || (from150 && totalJobs >= 150); }).ToList(); return ( filtered.Skip((page - 1) * pageSize).Take(pageSize), filtered.Count); } var totalCount = await query.CountAsync(); var items = await query .Include(v => v.Dispatches!) .ThenInclude(dispatch => dispatch.DispatchWorkOrders) .OrderByDescending(v => v.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (items, totalCount); } private static int CountDistinctWorkOrders(Vendor vendor) { if (vendor.Dispatches == null) return 0; return vendor.Dispatches .SelectMany(dispatch => { var linked = dispatch.DispatchWorkOrders?.Select(item => item.WorkOrderId) ?? Enumerable.Empty(); return dispatch.WorkOrderId.HasValue ? linked.Append(dispatch.WorkOrderId.Value) : linked; }) .Distinct() .Count(); } private static readonly MethodInfo StringConcatMethod = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!; private static Expression> BuildTradePredicate( IReadOnlyCollection selectedTrades) { var vendor = Expression.Parameter(typeof(Vendor), "vendor"); var specialties = Expression.Coalesce( Expression.Property(vendor, nameof(Vendor.TradeSpecialties)), Expression.Constant(string.Empty)); var lowered = Expression.Call(specialties, nameof(string.ToLower), Type.EmptyTypes); var delimiters = new[] { ", ", "," }; Expression? body = null; foreach (var trade in selectedTrades) { var token = Expression.Constant(trade); body = OrElse(body, Expression.Equal(lowered, token)); foreach (var delimiter in delimiters) { var delim = Expression.Constant(delimiter); var tokenWithTrailing = Concat(token, delim); var leadingToken = Concat(delim, token); var wrappedToken = Concat(leadingToken, delim); body = OrElse(body, StartsWith(lowered, tokenWithTrailing)); body = OrElse(body, EndsWith(lowered, leadingToken)); body = OrElse(body, Contains(lowered, wrappedToken)); } } return Expression.Lambda>(body ?? Expression.Constant(false), vendor); } private static Expression> BuildLocationPredicate( IReadOnlyCollection selectedLocations) { var vendor = Expression.Parameter(typeof(Vendor), "vendor"); var label = BuildLocationLabelBody(vendor); var lowered = Expression.Call(label, nameof(string.ToLower), Type.EmptyTypes); Expression? body = null; foreach (var location in selectedLocations) body = OrElse(body, Expression.Equal(lowered, Expression.Constant(location))); return Expression.Lambda>(body ?? Expression.Constant(false), vendor); } private static Expression BuildLocationLabelBody(ParameterExpression vendor) { var city = Expression.Property(vendor, nameof(Vendor.City)); var state = Expression.Property(vendor, nameof(Vendor.State)); var cityBlank = IsBlank(city); var stateBlank = IsBlank(state); var cityCommaState = Concat(Concat(city, Expression.Constant(", ")), state); var onlyState = Expression.Condition(stateBlank, Expression.Constant(string.Empty), state); var cityOrBoth = Expression.Condition(stateBlank, city, cityCommaState); return Expression.Condition(cityBlank, onlyState, cityOrBoth); } private static Expression IsBlank(Expression value) { var isNull = Expression.Equal(value, Expression.Constant(null, value.Type)); var trimmed = Expression.Call(value, nameof(string.Trim), Type.EmptyTypes); var isEmpty = Expression.Equal(trimmed, Expression.Constant(string.Empty)); return Expression.Condition(isNull, Expression.Constant(true), isEmpty); } private static Expression Concat(Expression left, Expression right) => Expression.Call(StringConcatMethod, left, right); private static Expression StartsWith(Expression instance, Expression value) => Expression.Call(instance, nameof(string.StartsWith), Type.EmptyTypes, value); private static Expression EndsWith(Expression instance, Expression value) => Expression.Call(instance, nameof(string.EndsWith), Type.EmptyTypes, value); private static Expression Contains(Expression instance, Expression value) => Expression.Call(instance, nameof(string.Contains), Type.EmptyTypes, value); private static Expression OrElse(Expression? accumulated, Expression next) => accumulated is null ? next : Expression.OrElse(accumulated, next); public async Task AddAsync(Vendor vendor) { await _context.Vendors.AddAsync(vendor); await _context.SaveChangesAsync(); return vendor; } public async Task UpdateAsync(Vendor vendor) { _context.Vendors.Update(vendor); await _context.SaveChangesAsync(); } public async Task DeleteAsync(int id) { var entity = await GetByIdAsync(id); if (entity != null) { _context.Vendors.Remove(entity); await _context.SaveChangesAsync(); } } public async Task ExistsAsync(int id) { return await _context.Vendors.AnyAsync(v => v.Id == id); } public async Task GetActiveTokenAsync(int vendorId) { return await _context.VendorAccessTokens .FirstOrDefaultAsync(t => t.VendorId == vendorId); } public async Task GetTokenByStringAsync(string token) { return await _context.VendorAccessTokens .FirstOrDefaultAsync(t => t.Token == token); } public async Task CountAsync() { return await _context.Vendors.CountAsync(); } } }