using AmpDAL.CustomCode.ApiRequestDuplicateCheck; using AmpDAL.CustomCode.ApiRequest; using AmpDAL.DTO.ApiRequestDuplicateCheck; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GB5Constant; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; namespace AMPBLL.ApiRequestDuplicateCheck; public class ApiRequestDuplicateCheckBLL( IApiRequestDuplicateCheckDAL _dal, IApiRequestDAL _requestDal, AutoNumber _autoNumber, EventLogPublish _eventLog, ILogger _logger ) : IApiRequestDuplicateCheckBLL { private static readonly HashSet StopWords = ["the", "a", "an", "to", "of", "in", "for", "and", "or", "by", "with", "get", "set", "list", "api"]; public async Task> GetPotentialDuplicatesAsync( int? apiRequestId, LoginDTO login, CancellationToken ct) { try { return apiRequestId.HasValue ? await _dal.GetByRequestAsync(apiRequestId.Value, login.ClientId, login, ct).ConfigureAwait(false) : await _dal.GetPendingAsync(login.ClientId, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetPotentialDuplicates failed TenantId {TenantId}", login.ClientId); throw; } } public async Task<(int Count, List TopMatches)> RunDuplicateCheckAsync( int apiRequestId, LoginDTO login, CancellationToken ct) { // ✅ Guard if (apiRequestId == 0) throw new ArgumentException("ApiRequestId must be a positive number."); try { var request = await _requestDal.GetRequestAsync(apiRequestId, login.ClientId, login, ct).ConfigureAwait(false); if (request is null) return (0, []); var keywords = ExtractKeywords(request.IntentKeywords ?? request.RequestTitle); var resourceNoun = ExtractResourceNoun(request.RequestTitle); var (kw1, kw2, kw3, kw4, kw5) = PadKeywords(keywords); // Run both searches in parallel var apiMatchTask = _dal.FindSimilarityInApiAsync( request.RequestTitle, request.TargetDomainId, request.ProposedApiType, resourceNoun, kw1, kw2, kw3, kw4, kw5, login.ClientId, login, ct); var requestMatchTask = _dal.FindSimilarityInRequestsAsync( request.RequestTitle, apiRequestId, request.TargetDomainId, request.ProposedApiType, kw1, kw2, kw3, kw4, kw5, login.ClientId, login, ct); await Task.WhenAll(apiMatchTask, requestMatchTask).ConfigureAwait(false); var apiMatches = apiMatchTask.Result; var requestMatches = requestMatchTask.Result; // ✅ Guard — skip AutoNumber if no matches int totalMatches = apiMatches.Count + requestMatches.Count; if (totalMatches == 0) { _logger.LogInformation( "RunDuplicateCheck found no matches for ApiRequestId {ApiRequestId}", apiRequestId); return (0, []); } // ✅ GetNumberAsync — consistent with rest of system var autoNumberDto = await _autoNumber.GetNumberAsync(totalMatches, "MAPIREQUESTDUPLICATECHECK", login) .ConfigureAwait(false); int nextId = autoNumberDto.StartNumber; var savedChecks = new List(); // ✅ Batch all inserts via Task.WhenAll var saveTasks = new List(); foreach (var match in apiMatches) { ct.ThrowIfCancellationRequested(); var opCount = await _dal.GetOperationCountAsync(match.MatchedId, login.ClientId, login, ct).ConfigureAwait(false); var avgParams = await _dal.GetAvgParameterCountAsync(match.MatchedId, login.ClientId, login, ct).ConfigureAwait(false); var (suggestionType, suggestedChange) = DetermineSuggestion(match, opCount, avgParams, isMapi: true); var dto = BuildSaveDto(nextId++, apiRequestId, match, 0, suggestionType, suggestedChange, login); savedChecks.Add(ToReadDto(dto, match.MatchedName)); saveTasks.Add(_dal.SaveDuplicateCheckAsync(dto, login, ct)); } foreach (var match in requestMatches) { ct.ThrowIfCancellationRequested(); var (suggestionType, suggestedChange) = DetermineSuggestion(match, 0, 0, isMapi: false); var dto = BuildSaveDto(nextId++, apiRequestId, match, 1, suggestionType, suggestedChange, login); savedChecks.Add(ToReadDto(dto, match.MatchedName)); saveTasks.Add(_dal.SaveDuplicateCheckAsync(dto, login, ct)); } await Task.WhenAll(saveTasks).ConfigureAwait(false); try { await _eventLog.PublishEventLogAsync( "AMP Duplicate Check Completed", new { apiRequestId, MatchesFound = savedChecks.Count }, Constant.EventTypeConstant.SAVEDOCUMENTSET, 0, login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "Event publish failed for duplicate check {ApiRequestId}", apiRequestId); } return (savedChecks.Count, savedChecks.OrderByDescending(x => x.OverallScore).Take(5).ToList()); } catch (Exception ex) { _logger.LogError(ex, "RunDuplicateCheck failed for ApiRequestId {ApiRequestId}", apiRequestId); throw; } } public async Task ReviewDuplicateAsync( int duplicateCheckId, byte reviewStatus, string? reviewRemarks, LoginDTO login, CancellationToken ct) { // ✅ Guards if (duplicateCheckId == 0) throw new ArgumentException("DuplicateCheckId must be a positive number."); if (reviewStatus is < 1 or > 4) throw new ArgumentException("ReviewStatus must be 1–4 (ConfirmedDuplicate/NotDuplicate/WillExtend/WillCreateNew)."); try { await _dal.UpdateReviewStatusAsync(duplicateCheckId, reviewStatus, login.UserId, reviewRemarks, login, ct) .ConfigureAwait(false); try { await _eventLog.PublishEventLogAsync( "AMP Duplicate Review Completed", new { duplicateCheckId, reviewStatus }, Constant.EventTypeConstant.SAVEDOCUMENTSET, 0, login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "Event publish failed for review {DuplicateCheckId}", duplicateCheckId); } return $"{SuccessResponse.UpdateSuccess} {duplicateCheckId}"; } catch (Exception ex) { _logger.LogError(ex, "ReviewDuplicate failed for DuplicateCheckId {DuplicateCheckId}", duplicateCheckId); throw; } } // ── Private helpers ──────────────────────────────────────────── private List ExtractKeywords(string? input) { if (string.IsNullOrWhiteSpace(input)) return []; return input .Split([',', ' ', '-', '_'], StringSplitOptions.RemoveEmptyEntries) .Select(w => w.ToLowerInvariant().Trim()) .Where(w => w.Length > 2 && !StopWords.Contains(w)) .Distinct() .Take(5) .ToList(); } private static string ExtractResourceNoun(string title) { var words = title.Split(' ', StringSplitOptions.RemoveEmptyEntries); return words.FirstOrDefault(w => w.Length > 3) ?? string.Empty; } private static (string, string, string, string, string) PadKeywords(List kws) { while (kws.Count < 5) kws.Add(string.Empty); return (kws[0], kws[1], kws[2], kws[3], kws[4]); } private static (byte SuggestionType, string SuggestedChange) DetermineSuggestion( SimilarityResultDTO match, int opCount, double avgParams, bool isMapi) { if (match.OverallScore >= 75) return (0, $"'{match.MatchedName}' appears to duplicate an existing {(isMapi ? "API" : "request")} with score {match.OverallScore}. Review before proceeding."); if (!isMapi) return (0, $"Open request '{match.MatchedName}' covers similar scope (score {match.OverallScore}). Coordinate with the existing request owner."); if (opCount >= 15) return (4, $"'{match.MatchedName}' already has {opCount} operations. Consider creating a new API version rather than extending further."); if (opCount >= 8) return (3, $"'{match.MatchedName}' has {opCount} operations. Adding a new endpoint under this API is feasible."); if (avgParams >= 6) return (2, $"'{match.MatchedName}' has complex parameter sets (avg {avgParams:F1} params/op). Consider adding optional response fields instead of a new endpoint."); return (1, $"'{match.MatchedName}' covers related scope. Consider adding optional query parameters to an existing endpoint rather than a new service."); } private static ApiRequestDuplicateCheckSaveDTO BuildSaveDto( int id, int sourceRequestId, SimilarityResultDTO match, byte matchSource, byte suggestionType, string suggestedChange, LoginDTO login) { return new ApiRequestDuplicateCheckSaveDTO { DuplicateCheckId = id, TenantId = login.ClientId, SourceApiRequestId = sourceRequestId, MatchSource = matchSource, MatchedApiId = matchSource == 0 ? match.MatchedId : -1, MatchedRequestId = matchSource == 1 ? match.MatchedId : -1, MatchedInterfaceId = -1, OverallScore = match.OverallScore, NameScore = match.NameScore, DomainScore = match.DomainScore, IntentScore = match.IntentScore, SchemaScore = match.SchemaScore, SuggestionType = suggestionType, SuggestedChange = suggestedChange, MatchReason = $"Name:{match.NameScore} Domain:{match.DomainScore} Intent:{match.IntentScore} Schema:{match.SchemaScore}", ReviewStatus = 0 }; } private static ApiRequestDuplicateCheckDTO ToReadDto(ApiRequestDuplicateCheckSaveDTO dto, string matchedName) { return new ApiRequestDuplicateCheckDTO { DuplicateCheckId = dto.DuplicateCheckId, TenantId = dto.TenantId, SourceApiRequestId = dto.SourceApiRequestId, MatchSource = dto.MatchSource, MatchedApiId = dto.MatchedApiId, MatchedRequestId = dto.MatchedRequestId, MatchedInterfaceId = dto.MatchedInterfaceId, OverallScore = dto.OverallScore, NameScore = dto.NameScore, DomainScore = dto.DomainScore, IntentScore = dto.IntentScore, SchemaScore = dto.SchemaScore, SuggestionType = dto.SuggestionType, SuggestedChange = dto.SuggestedChange, MatchReason = dto.MatchReason, ReviewStatus = 0 // ✅ CreatedOn removed — let DB DEFAULT handle it }; } }