using AsbCloudApp.Data; using AsbCloudApp.Data.WellOperation; using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Requests; using AsbCloudApp.Services; using AsbCloudDb; using AsbCloudDb.Model; using Mapster; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Repository; public class WellOperationRepository : CrudRepositoryBase, IWellOperationRepository { private const string cacheKeyWellOperations = "FirstAndLastFactWellsOperations"; private readonly IMemoryCache memoryCache; private readonly IWellOperationCategoryRepository wellOperationCategoryRepository; private readonly IWellService wellService; private Lazy> LazyWellCategories { get; } private Lazy> LazyWellSectionTypes { get; } public WellOperationRepository(IAsbCloudDbContext context, IMemoryCache memoryCache, IWellOperationCategoryRepository wellOperationCategoryRepository, IWellService wellService) : base(context, dbSet => dbSet) { this.memoryCache = memoryCache; this.wellOperationCategoryRepository = wellOperationCategoryRepository; this.wellService = wellService; LazyWellCategories = new(() => wellOperationCategoryRepository.Get(true, false).ToDictionary(c => c.Id)); LazyWellSectionTypes = new(() => GetSectionTypes().ToDictionary(c => c.Id)); } public IEnumerable GetSectionTypes() => memoryCache .GetOrCreateBasic(dbContext.WellSectionTypes) .OrderBy(s => s.Order) .Select(s => s.Adapt()); public async Task> GetAsync(WellOperationRequest request, CancellationToken token) { var (items, _) = await GetWithDaysAndNpvAsync(request, token); return items; } public async Task> GetPageAsync(WellOperationRequest request, CancellationToken token) { request.Skip = request.Skip ?? 0; request.Take = request.Take ?? 32; var (items, count) = await GetWithDaysAndNpvAsync(request, token); var paginationContainer = new PaginationContainer { Skip = request.Skip!.Value, Take = request.Take!.Value, Count = count, Items = items }; return paginationContainer; } public async Task?> GetPageAsync(int idWell, int id, int operationType, int? take, IEnumerable? sortFields, CancellationToken token) { var request = new WellOperationRequest(new[] { idWell }) { OperationType = operationType, SortFields = sortFields, }; var (wellOperations, count) = await GetWithDaysAndNpvAsync(request, token); var skip = 0; take ??= 32; while (skip < count) { var page = wellOperations.Skip(skip) .Take(take.Value); if (page.Any(x => x.Id == id)) { var paginationContainer = new PaginationContainer { Skip = skip, Take = take.Value, Items = page, Count = count }; return paginationContainer; } skip += take.Value; } return null; } public async Task> GetGroupOperationsStatAsync(WellOperationRequest request, CancellationToken token) { var query = BuildQuery(request); var entities = await query .Select(o => new { o.IdCategory, DurationMinutes = o.DurationHours * 60, DurationDepth = o.DepthEnd - o.DepthStart }) .ToArrayAsync(token); var parentRelationDictionary = wellOperationCategoryRepository.Get(true) .ToDictionary(c => c.Id, c => new { c.Name, c.IdParent }); var dtos = entities .GroupBy(o => o.IdCategory) .Select(g => new WellGroupOpertionDto { IdCategory = g.Key, Category = parentRelationDictionary[g.Key].Name, Count = g.Count(), MinutesAverage = g.Average(o => o.DurationMinutes), MinutesMin = g.Min(o => o.DurationMinutes), MinutesMax = g.Max(o => o.DurationMinutes), TotalMinutes = g.Sum(o => o.DurationMinutes), DeltaDepth = g.Sum(o => o.DurationDepth), IdParent = parentRelationDictionary[g.Key].IdParent }); while (dtos.All(x => x.IdParent != null)) { dtos = dtos .GroupBy(o => o.IdParent!) .Select(g => { var idCategory = g.Key ?? int.MinValue; var category = parentRelationDictionary.GetValueOrDefault(idCategory); var newDto = new WellGroupOpertionDto { IdCategory = idCategory, Category = category?.Name ?? "unknown", Count = g.Sum(o => o.Count), DeltaDepth = g.Sum(o => o.DeltaDepth), TotalMinutes = g.Sum(o => o.TotalMinutes), Items = g.ToList(), IdParent = category?.IdParent, }; return newDto; }); } return dtos; } public async Task InsertRangeAsync(IEnumerable dtos, bool deleteBeforeInsert, CancellationToken token) { EnsureValidWellOperations(dtos); var result = 0; if (!deleteBeforeInsert) { result = await InsertRangeAsync(dtos, token); if (result > 0) memoryCache.Remove(cacheKeyWellOperations); return result; } var idType = dtos.First().IdType; var idWell = dtos.First().IdWell; var existingOperationIds = await dbContext.WellOperations .Where(e => e.IdWell == idWell && e.IdType == idType) .Select(e => e.Id) .ToArrayAsync(token); await DeleteRangeAsync(existingOperationIds, token); result = await InsertRangeAsync(dtos, token); if (result > 0) memoryCache.Remove(cacheKeyWellOperations); return result; } public override async Task UpdateRangeAsync(IEnumerable dtos, CancellationToken token) { EnsureValidWellOperations(dtos); var result = await base.UpdateRangeAsync(dtos, token); if (result > 0) memoryCache.Remove(cacheKeyWellOperations); return result; } private static void EnsureValidWellOperations(IEnumerable dtos) { if (dtos.GroupBy(d => d.IdType).Count() > 1) throw new ArgumentInvalidException(nameof(dtos), "Все операции должны быть одного типа"); if (dtos.GroupBy(d => d.IdType).Count() > 1) throw new ArgumentInvalidException(nameof(dtos), "Все операции должны принадлежать одной скважине"); } private async Task> GetByIdsWells(IEnumerable idsWells, CancellationToken token) { var query = GetQuery() .Where(e => idsWells.Contains(e.IdWell)) .OrderBy(e => e.DateStart); var entities = await query.ToArrayAsync(token); return entities; } private async Task<(IEnumerable items, int count)> GetWithDaysAndNpvAsync(WellOperationRequest request, CancellationToken token) { var entities = await GetByIdsWells(request.IdsWell, token); var groupedByWellAndType = entities .GroupBy(e => new { e.IdWell, e.IdType }); var result = new List(); var count = 0; foreach (var wellOperationsWithType in groupedByWellAndType) { var firstWellOperation = wellOperationsWithType.MinBy(e => e.DateStart); var operationsWithNpt = wellOperationsWithType .Where(o => WellOperationCategory.NonProductiveTimeSubIds.Contains(o.IdCategory)); IEnumerable filteredWellOperations = FilterByRequest(wellOperationsWithType.AsQueryable(), request); count += filteredWellOperations.Count(); if (request.Skip != null) filteredWellOperations = filteredWellOperations.Skip((int)request.Skip); if (request.Take != null) filteredWellOperations = filteredWellOperations.Take((int)request.Take); var timezoneOffset = wellService.GetTimezone(wellOperationsWithType.Key.IdWell).Offset; var dtos = filteredWellOperations .Select(entity => { var dto = Convert(entity, timezoneOffset); dto.Day = (entity.DateStart - firstWellOperation.DateStart).TotalDays; dto.NptHours = operationsWithNpt .Where(o => o.DateStart <= entity.DateStart) .Sum(e => e.DurationHours); return dto; }); result.AddRange(dtos); } return (result, count); } private static IQueryable FilterByRequest(IQueryable entities, WellOperationRequest request) { if (request.OperationType.HasValue) entities = entities.Where(e => e.IdType == request.OperationType.Value); if (request.SectionTypeIds?.Any() is true) entities = entities.Where(e => request.SectionTypeIds.Contains(e.IdWellSectionType)); if (request.OperationCategoryIds?.Any() is true) entities = entities.Where(e => request.OperationCategoryIds.Contains(e.IdCategory)); if (request.GeDepth.HasValue) entities = entities.Where(e => e.DepthEnd >= request.GeDepth.Value); if (request.LeDepth.HasValue) entities = entities.Where(e => e.DepthEnd <= request.LeDepth.Value); if (request.GeDate.HasValue) { var geDateUtc = request.GeDate.Value.UtcDateTime; entities = entities.Where(e => e.DateStart >= geDateUtc); } if (request.LeDate.HasValue) { var leDateUtc = request.LeDate.Value.UtcDateTime; entities = entities.Where(e => e.DateStart <= leDateUtc); } if (request.SortFields?.Any() is true) entities = entities.AsQueryable().SortBy(request.SortFields); else entities = entities.AsQueryable().OrderBy(e => e.DateStart); return entities; } private IQueryable BuildQuery(WellOperationRequest request) { var query = GetQuery() .Where(e => request.IdsWell.Contains(e.IdWell)) .OrderBy(e => e.DateStart) .AsQueryable(); query = FilterByRequest(query, request); return query; } public async Task> GetSectionsAsync(IEnumerable idsWells, CancellationToken token) { const string keyCacheSections = "OperationsBySectionSummarties"; var cache = await memoryCache.GetOrCreateAsync(keyCacheSections, async (entry) => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); var query = dbContext.Set() .GroupBy(operation => new { operation.IdWell, operation.IdType, operation.IdWellSectionType, operation.WellSectionType.Caption, }) .Select(group => new { group.Key.IdWell, group.Key.IdType, group.Key.IdWellSectionType, group.Key.Caption, First = group .OrderBy(operation => operation.DateStart) .Select(operation => new { operation.DateStart, operation.DepthStart, }) .First(), Last = group .OrderByDescending(operation => operation.DateStart) .Select(operation => new { operation.DateStart, operation.DurationHours, operation.DepthEnd, }) .First(), }) .Where(s => idsWells.Contains(s.IdWell)); var dbData = await query.ToArrayAsync(token); var sections = dbData.Select( item => new SectionByOperationsDto { IdWell = item.IdWell, IdType = item.IdType, IdWellSectionType = item.IdWellSectionType, Caption = item.Caption, DateStart = item.First.DateStart, DepthStart = item.First.DepthStart, DateEnd = item.Last.DateStart.AddHours(item.Last.DurationHours), DepthEnd = item.Last.DepthEnd, }) .ToArray() .AsEnumerable(); entry.Value = sections; return sections; }); return cache!; } public async Task GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken) { var query = dbContext.WellOperations.Where(o => o.IdWell == idWell && o.IdType == idType); if (!await query.AnyAsync(cancellationToken)) return null; var timeZoneOffset = wellService.GetTimezone(idWell).Offset; var minDate = await query.MinAsync(o => o.DateStart, cancellationToken); var maxDate = await query.MaxAsync(o => o.DateStart, cancellationToken); return new DatesRangeDto { From = minDate.ToOffset(timeZoneOffset), To = maxDate.ToOffset(timeZoneOffset) }; } public (WellOperationDto First, WellOperationDto Last)? GetFirstAndLastFact(int idWell) { var cachedDictionary = memoryCache.GetOrCreate(cacheKeyWellOperations, (entry) => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); var query = dbContext.Set() .Where(o => o.IdType == WellOperation.IdOperationTypeFact) .GroupBy(o => o.IdWell) .Select(group => new { IdWell = group.Key, FirstFact = group.OrderBy(o => o.DateStart).First(), LastFact = group.OrderBy(o => o.DateStart).Last(), }); var entities = query.ToArray(); var dictionary = entities.ToDictionary(s => s.IdWell, s => (Convert(s.FirstFact), Convert(s.LastFact))); entry.Value = dictionary; return dictionary; })!; var firstAndLast = cachedDictionary.GetValueOrDefault(idWell); return firstAndLast; } public override async Task DeleteAsync(int id, CancellationToken token) { var result = await base.DeleteAsync(id, token); if (result > 0) memoryCache.Remove(cacheKeyWellOperations); return result; } public override async Task DeleteRangeAsync(IEnumerable ids, CancellationToken token) { var result = await base.DeleteRangeAsync(ids, token); if (result > 0) memoryCache.Remove(cacheKeyWellOperations); return result; } protected override WellOperation Convert(WellOperationDto src) { var entity = src.Adapt(); entity.DateStart = src.DateStart.UtcDateTime; return entity; } private WellOperationDto Convert(WellOperation src, TimeSpan timezoneOffset) { var dto = src.Adapt(); dto.DateStart = src.DateStart.ToOffset(timezoneOffset); dto.LastUpdateDate = src.LastUpdateDate.ToOffset(timezoneOffset); dto.OperationCategoryName = LazyWellCategories.Value.TryGetValue(src.IdCategory, out WellOperationCategoryDto? category) ? category.Name : string.Empty; dto.WellSectionTypeCaption = LazyWellSectionTypes.Value.TryGetValue(src.IdWellSectionType, out WellSectionTypeDto? sectionType) ? sectionType.Caption : string.Empty; return dto; } }