using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Types; using ScadaLink.Commons.Types.Flattening; using ScadaLink.TemplateEngine.Flattening; using ScadaLink.TemplateEngine.Validation; namespace ScadaLink.DeploymentManager; /// /// Orchestrates the TemplateEngine services (FlatteningService, ValidationService, RevisionHashService) /// into a single pipeline for deployment use. /// /// WP-16: This captures template state at the time of flatten, ensuring that concurrent template edits /// (last-write-wins) do not conflict with in-progress deployments. /// public class FlatteningPipeline : IFlatteningPipeline { private readonly ITemplateEngineRepository _templateRepo; private readonly ISiteRepository _siteRepo; private readonly FlatteningService _flatteningService; private readonly ValidationService _validationService; private readonly RevisionHashService _revisionHashService; public FlatteningPipeline( ITemplateEngineRepository templateRepo, ISiteRepository siteRepo, FlatteningService flatteningService, ValidationService validationService, RevisionHashService revisionHashService) { _templateRepo = templateRepo; _siteRepo = siteRepo; _flatteningService = flatteningService; _validationService = validationService; _revisionHashService = revisionHashService; } public async Task> FlattenAndValidateAsync( int instanceId, CancellationToken cancellationToken = default) { // Load instance with full graph var instance = await _templateRepo.GetInstanceByIdAsync(instanceId, cancellationToken); if (instance == null) return Result.Failure($"Instance with ID {instanceId} not found."); // Build template chain var templateChain = await BuildTemplateChainAsync(instance.TemplateId, cancellationToken); if (templateChain.Count == 0) return Result.Failure("Template chain is empty."); // Build composition maps, walking nested compositions so the flattener // can resolve composed-of-composed attributes / alarms / scripts (e.g. // a parent that composes Pump where Pump itself composes AlarmSensor // produces "Pump.AlarmSensor.SensorReading"). var compositionMap = new Dictionary>(); var composedChains = new Dictionary>(); var processedTemplateIds = new HashSet(); var pendingChains = new Queue>(); pendingChains.Enqueue(templateChain); while (pendingChains.Count > 0) { var chain = pendingChains.Dequeue(); foreach (var template in chain) { if (!processedTemplateIds.Add(template.Id)) continue; var compositions = await _templateRepo.GetCompositionsByTemplateIdAsync(template.Id, cancellationToken); if (compositions.Count == 0) continue; compositionMap[template.Id] = compositions; foreach (var comp in compositions) { if (composedChains.ContainsKey(comp.ComposedTemplateId)) continue; var composedChain = await BuildTemplateChainAsync(comp.ComposedTemplateId, cancellationToken); composedChains[comp.ComposedTemplateId] = composedChain; pendingChains.Enqueue(composedChain); } } } // Load data connections for the site var dataConnections = await LoadDataConnectionsAsync(instance.SiteId, cancellationToken); // Flatten var flattenResult = _flatteningService.Flatten( instance, templateChain, compositionMap, composedChains, dataConnections); if (flattenResult.IsFailure) return Result.Failure(flattenResult.Error); var config = flattenResult.Value; // Load shared scripts for semantic validation var sharedScriptEntities = await _templateRepo.GetAllSharedScriptsAsync(cancellationToken); var resolvedSharedScripts = sharedScriptEntities.Select(s => new ResolvedScript { CanonicalName = s.Name, Code = s.Code, ParameterDefinitions = s.ParameterDefinitions, ReturnDefinition = s.ReturnDefinition }).ToList(); // Validate var validation = _validationService.Validate(config, resolvedSharedScripts); // Compute revision hash var hash = _revisionHashService.ComputeHash(config); return Result.Success( new FlatteningPipelineResult(config, hash, validation)); } private async Task> BuildTemplateChainAsync( int templateId, CancellationToken cancellationToken) { var chain = new List(); var currentId = (int?)templateId; while (currentId.HasValue) { var template = await _templateRepo.GetTemplateWithChildrenAsync(currentId.Value, cancellationToken); if (template == null) break; chain.Add(template); currentId = template.ParentTemplateId; } return chain; } private async Task> LoadDataConnectionsAsync( int siteId, CancellationToken cancellationToken) { var connections = await _siteRepo.GetDataConnectionsBySiteIdAsync(siteId, cancellationToken); return connections.ToDictionary(c => c.Id); } }