using System.Runtime.CompilerServices; using Dapper; using JdeScoping.Core.Interfaces; using JdeScoping.DataAccess.Options; using JdeScoping.DataAccess.Interfaces; using JdeScoping.Core.Models.SearchResults; using JdeScoping.DataAccess.Models; using JdeScoping.DataAccess.QueryBuilders; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SqlKata.Compilers; namespace JdeScoping.DataAccess.Services; /// /// Main search processor service that orchestrates search execution. /// public sealed class SearchProcessor : ISearchProcessor { private readonly IDbConnectionFactory _connectionFactory; private readonly ISearchQueryBuilder _queryBuilder; private readonly IWorkOrderTraversalService _traversalService; private readonly MisQueryBuilder _misQueryBuilder; private readonly SearchProcessingConfiguration _options; private readonly ILogger _logger; /// /// Initializes a new instance of SearchProcessor. /// public SearchProcessor( IDbConnectionFactory connectionFactory, ISearchQueryBuilder queryBuilder, IWorkOrderTraversalService traversalService, SqlServerCompiler compiler, IOptions options, ILogger logger) { _connectionFactory = connectionFactory; _queryBuilder = queryBuilder; _traversalService = traversalService; _misQueryBuilder = new MisQueryBuilder(compiler); _options = options.Value; _logger = logger; } /// /// Executes search and returns results as async stream. /// /// The search model containing filter criteria. /// Cancellation token. /// Async enumerable of search results. public async IAsyncEnumerable ExecuteSearchAsync( SearchModel model, [EnumeratorCancellation] CancellationToken ct = default) { _logger.LogInformation("Executing search {SearchId}", model.Id); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); // Build the search query using searchId only var queryResult = _queryBuilder.BuildSearchQuery(model.Id); if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath)) { await WriteDebugSqlAsync(model.Id, queryResult); } // Execute setup SQL (temp tables, filter population) foreach (var setupSql in queryResult.TempTableSetupSql) { _logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]); await connection.ExecuteAsync( setupSql, queryResult.Parameters, commandTimeout: _options.QueryTimeoutSeconds); } // Execute downstream traversal await _traversalService.TraverseDownstreamAsync( connection, _options.MaxTraversalIterations, ct); // Stream results using unbuffered query _logger.LogDebug("Executing result query"); var reader = await connection.QueryAsync( queryResult.Sql, queryResult.Parameters, commandTimeout: _options.QueryTimeoutSeconds); foreach (var result in reader) { ct.ThrowIfCancellationRequested(); yield return result; } _logger.LogInformation("Search {SearchId} completed", model.Id); } /// /// Executes search and materializes all results into SearchModel. /// /// The search model containing filter criteria. /// Cancellation token. /// The SearchModel populated with results. public async Task ExecuteSearchToModelAsync( SearchModel model, CancellationToken ct = default) { _logger.LogInformation("Executing search {SearchId} to model", model.Id); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); // Build the search query using searchId only var queryResult = _queryBuilder.BuildSearchQuery(model.Id); if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath)) { await WriteDebugSqlAsync(model.Id, queryResult); } // Execute setup SQL (temp tables, filter population) foreach (var setupSql in queryResult.TempTableSetupSql) { _logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]); await connection.ExecuteAsync( setupSql, queryResult.Parameters, commandTimeout: _options.QueryTimeoutSeconds); } // Execute downstream traversal await _traversalService.TraverseDownstreamAsync( connection, _options.MaxTraversalIterations, ct); // Execute result query and materialize _logger.LogDebug("Executing result query"); var results = await connection.QueryAsync( queryResult.Sql, queryResult.Parameters, commandTimeout: _options.QueryTimeoutSeconds); model.Results = results.ToList(); _logger.LogInformation("Search {SearchId} returned {ResultCount} results", model.Id, model.Results.Count); // Extract MIS data if requested (check ExtractMisData from database using extraction function) var extractMisData = await connection.QuerySingleOrDefaultAsync( "SELECT dbo.fn_GetSearchExtractMisData(@SearchId)", new { SearchId = model.Id }, commandTimeout: _options.QueryTimeoutSeconds) ?? false; if (extractMisData) { await ExecuteMisExtractionAsync(model, connection, ct); } return model; } private async Task ExecuteMisExtractionAsync( SearchModel model, SqlConnection connection, CancellationToken ct) { _logger.LogDebug("Extracting MIS data for search {SearchId}", model.Id); // Build and execute MIS setup SQL (uses temp tables and variables from main query) var misSetupStatements = _misQueryBuilder.BuildMisExtractionSql(model.Id); foreach (var sql in misSetupStatements) { await connection.ExecuteAsync( sql, new { SearchId = model.Id }, commandTimeout: _options.QueryTimeoutSeconds); } // Execute MIS result query var misQueryResult = _queryBuilder.BuildMisQuery(model.Id); var misResults = await connection.QueryAsync( misQueryResult.Sql, misQueryResult.Parameters, commandTimeout: _options.QueryTimeoutSeconds); model.MisResults = misResults.ToList(); _logger.LogDebug("Found {MisResultCount} MIS results", model.MisResults.Count); // Execute MIS non-match query var misNonMatchQueryResult = _queryBuilder.BuildMisNonMatchQuery(model.Id); var misNonMatchResults = await connection.QueryAsync( misNonMatchQueryResult.Sql, misNonMatchQueryResult.Parameters, commandTimeout: _options.QueryTimeoutSeconds); model.MisNonMatchResults = misNonMatchResults.ToList(); _logger.LogDebug("Found {MisNonMatchCount} MIS non-match results", model.MisNonMatchResults.Count); } private async Task WriteDebugSqlAsync(int searchId, SearchQueryResult queryResult) { try { var debugPath = Path.Combine(_options.DebugSqlPath!, $"search_{searchId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.sql"); var content = string.Join("\r\n\r\n-- ========================================\r\n\r\n", queryResult.TempTableSetupSql.Append(queryResult.Sql)); await File.WriteAllTextAsync(debugPath, content); _logger.LogDebug("Debug SQL written to {Path}", debugPath); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to write debug SQL"); } } }