001/*
002 * Copyright 2025 devteam@scivicslab.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing,
011 * software distributed under the License is distributed on an
012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied.  See the License for the
014 * specific language governing permissions and limitations
015 * under the License.
016 */
017
018package com.scivicslab.actoriac;
019
020import java.util.ArrayList;
021import java.util.List;
022import java.util.Map;
023import java.util.concurrent.CompletableFuture;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.ExecutionException;
026import java.util.concurrent.atomic.AtomicInteger;
027import java.util.logging.Level;
028import java.util.logging.Logger;
029import java.util.regex.Pattern;
030
031import org.json.JSONArray;
032import org.json.JSONObject;
033
034import com.scivicslab.pojoactor.core.Action;
035import com.scivicslab.pojoactor.core.ActionResult;
036import com.scivicslab.pojoactor.workflow.IIActorRef;
037import com.scivicslab.pojoactor.workflow.IIActorSystem;
038
039import com.scivicslab.actoriac.log.DistributedLogStore;
040
041/**
042 * Interpreter-interfaced actor reference for {@link NodeGroupInterpreter} instances.
043 *
044 * <p>This class provides a concrete implementation of {@link IIActorRef}
045 * specifically for {@link NodeGroupInterpreter} objects. It manages groups of infrastructure
046 * nodes and can apply actions to all nodes in a group using wildcard patterns.</p>
047 *
048 * <p>NodeGroupInterpreter extends Interpreter, so this class can execute main workflows
049 * that orchestrate multiple nodes.</p>
050 *
051 * <p><strong>Supported actions:</strong></p>
052 * <p><em>Workflow actions (from Interpreter):</em></p>
053 * <ul>
054 *   <li>{@code runWorkflow} - Loads and runs a workflow file</li>
055 *   <li>{@code readYaml} - Reads a YAML workflow definition</li>
056 *   <li>{@code runUntilEnd} - Executes the workflow until completion</li>
057 * </ul>
058 * <p><em>NodeGroup actions:</em></p>
059 * <ul>
060 *   <li>{@code hasInventory} - Returns true if inventory is loaded (for conditional branching)</li>
061 *   <li>{@code createNodeActors} - Creates child actors for all nodes in a specified group</li>
062 *   <li>{@code apply} - Applies an action to child actors matching a wildcard pattern</li>
063 *   <li>{@code hasAccumulator} - Returns true if accumulator exists (for idempotent workflows)</li>
064 *   <li>{@code createAccumulator} - Creates an accumulator for result collection</li>
065 *   <li>{@code getAccumulatorSummary} - Gets the collected results</li>
066 *   <li>{@code getSessionId} - Gets the current session ID for log queries</li>
067 * </ul>
068 *
069 * <p><strong>Node Actor Hierarchy:</strong></p>
070 * <p>When {@code createNodeActors} is called, it creates a parent-child relationship:</p>
071 * <pre>
072 * NodeGroup (parent)
073 *   ├─ node-web-01 (child NodeIIAR)
074 *   ├─ node-web-02 (child NodeIIAR)
075 *   └─ node-db-01 (child NodeIIAR)
076 * </pre>
077 *
078 * <p><strong>Example YAML Workflow:</strong></p>
079 * <pre>{@code
080 * name: setup-nodegroup
081 * steps:
082 *   # Step 1: Create node actors
083 *   - states: [0, 1]
084 *     actions:
085 *       - actor: nodeGroup
086 *         method: createNodeActors
087 *         arguments: ["web-servers"]
088 *
089 *   # Step 2: Run workflow on all nodes (load and execute in one step)
090 *   - states: [1, end]
091 *     actions:
092 *       - actor: nodeGroup
093 *         method: apply
094 *         arguments: ['{"actor": "node-*", "method": "runWorkflow", "arguments": ["deploy.yaml"]}']
095 * }</pre>
096 *
097 * @author devteam@scivicslab.com
098 */
099public class NodeGroupIIAR extends IIActorRef<NodeGroupInterpreter> {
100
101    Logger logger = null;
102
103    /** Current workflow file path being executed. */
104    private String currentWorkflowPath = null;
105
106    /**
107     * Constructs a new NodeGroupIIAR with the specified actor name and nodeGroupInterpreter object.
108     *
109     * @param actorName the name of this actor
110     * @param object the {@link NodeGroupInterpreter} instance managed by this actor reference
111     */
112    public NodeGroupIIAR(String actorName, NodeGroupInterpreter object) {
113        super(actorName, object);
114        logger = Logger.getLogger(actorName);
115    }
116
117    /**
118     * Constructs a new NodeGroupIIAR with the specified actor name, nodeGroupInterpreter object,
119     * and actor system.
120     *
121     * @param actorName the name of this actor
122     * @param object the {@link NodeGroupInterpreter} instance managed by this actor reference
123     * @param system the actor system managing this actor
124     */
125    public NodeGroupIIAR(String actorName, NodeGroupInterpreter object, IIActorSystem system) {
126        super(actorName, object, system);
127        logger = Logger.getLogger(actorName);
128
129        // Set the selfActorRef in the Interpreter (NodeGroupInterpreter extends Interpreter)
130        object.setSelfActorRef(this);
131    }
132
133    // ========================================================================
134    // Workflow Actions (from Interpreter)
135    // ========================================================================
136
137    /**
138     * Executes the current step code.
139     *
140     * @param args the argument string (not used)
141     * @return ActionResult indicating success or failure
142     */
143    @Action("execCode")
144    public ActionResult execCode(String args) {
145        try {
146            return this.ask(n -> n.execCode()).get();
147        } catch (InterruptedException | ExecutionException e) {
148            logger.log(Level.SEVERE, "execCode failed", e);
149            return new ActionResult(false, "Error: " + e.getMessage());
150        }
151    }
152
153    /**
154     * Runs the workflow until completion.
155     *
156     * @param args the argument string (optional max iterations)
157     * @return ActionResult indicating success or failure
158     */
159    @Action("runUntilEnd")
160    public ActionResult runUntilEnd(String args) {
161        try {
162            int maxIterations = parseMaxIterations(args, 10000);
163            return this.ask(n -> n.runUntilEnd(maxIterations)).get();
164        } catch (InterruptedException | ExecutionException e) {
165            logger.log(Level.SEVERE, "runUntilEnd failed", e);
166            return new ActionResult(false, "Error: " + e.getMessage());
167        }
168    }
169
170    /**
171     * Loads and runs a workflow file.
172     *
173     * @param args JSON array with workflow file path and optional max iterations
174     * @return ActionResult indicating success or failure
175     */
176    @Action("runWorkflow")
177    public ActionResult runWorkflow(String args) {
178        try {
179            JSONArray runArgs = new JSONArray(args);
180            String workflowFile = runArgs.getString(0);
181            this.currentWorkflowPath = workflowFile;  // Store for WorkflowReporter
182            int runMaxIterations = runArgs.length() > 1 ? runArgs.getInt(1) : 10000;
183            logger.info(String.format("Running workflow: %s (maxIterations=%d)", workflowFile, runMaxIterations));
184            ActionResult result = this.object.runWorkflow(workflowFile, runMaxIterations);
185            logger.info(String.format("Workflow completed: success=%s, result=%s", result.isSuccess(), result.getResult()));
186            return result;
187        } catch (Exception e) {
188            logger.log(Level.SEVERE, "runWorkflow failed: " + args, e);
189            return new ActionResult(false, "Error: " + e.getMessage());
190        }
191    }
192
193    /**
194     * Reads a YAML workflow definition.
195     *
196     * @param args JSON array with file path
197     * @return ActionResult indicating success or failure
198     */
199    @Action("readYaml")
200    public ActionResult readYaml(String args) {
201        String filePath = extractSingleArgument(args);
202        this.currentWorkflowPath = filePath;  // Store for WorkflowReporter
203        try {
204            String overlayPath = this.object.getOverlayDir();
205            if (overlayPath != null) {
206                java.nio.file.Path yamlPath = java.nio.file.Path.of(filePath);
207                java.nio.file.Path overlayDir = java.nio.file.Path.of(overlayPath);
208                this.tell(n -> {
209                    try {
210                        n.readYaml(yamlPath, overlayDir);
211                    } catch (java.io.IOException e) {
212                        throw new RuntimeException(e);
213                    }
214                }).get();
215                return new ActionResult(true, "YAML loaded with overlay: " + overlayPath);
216            } else {
217                try (java.io.InputStream input = new java.io.FileInputStream(new java.io.File(filePath))) {
218                    this.tell(n -> n.readYaml(input)).get();
219                    return new ActionResult(true, "YAML loaded successfully");
220                }
221            }
222        } catch (java.io.FileNotFoundException e) {
223            logger.log(Level.SEVERE, String.format("file not found: %s", filePath), e);
224            return new ActionResult(false, "File not found: " + filePath);
225        } catch (java.io.IOException e) {
226            logger.log(Level.SEVERE, String.format("IOException: %s", filePath), e);
227            return new ActionResult(false, "IO error: " + filePath);
228        } catch (RuntimeException e) {
229            if (e.getCause() instanceof java.io.IOException) {
230                logger.log(Level.SEVERE, String.format("IOException: %s", filePath), e.getCause());
231                return new ActionResult(false, "IO error: " + filePath);
232            }
233            throw e;
234        } catch (InterruptedException | ExecutionException e) {
235            logger.log(Level.SEVERE, "readYaml failed: " + filePath, e);
236            return new ActionResult(false, "Error: " + e.getMessage());
237        }
238    }
239
240    // ========================================================================
241    // NodeGroup-specific Actions
242    // ========================================================================
243
244    /**
245     * Checks if inventory is loaded.
246     *
247     * @param args the argument string (not used)
248     * @return ActionResult with true if inventory exists
249     */
250    @Action("hasInventory")
251    public ActionResult hasInventory(String args) {
252        boolean hasInv = this.object.getInventory() != null;
253        return new ActionResult(hasInv, hasInv ? "Inventory available" : "No inventory");
254    }
255
256    /**
257     * Creates child actors for all nodes in a specified group.
258     *
259     * @param args JSON array with group name
260     * @return ActionResult indicating success or failure
261     */
262    @Action("createNodeActors")
263    public ActionResult createNodeActorsAction(String args) {
264        String groupName = extractSingleArgument(args);
265        createNodeActors(groupName);
266        return new ActionResult(true, String.format("Created node actors for group '%s'", groupName));
267    }
268
269    /**
270     * Applies an action to child actors matching a wildcard pattern.
271     *
272     * @param args JSON object defining the action to apply
273     * @return ActionResult indicating success or failure
274     */
275    @Action("apply")
276    public ActionResult applyAction(String args) {
277        return apply(args);
278    }
279
280    /**
281     * Executes a command on all child node actors.
282     *
283     * @param args JSON array with command
284     * @return ActionResult with execution results
285     */
286    @Action("executeCommandOnAllNodes")
287    public ActionResult executeCommandOnAllNodesAction(String args) {
288        try {
289            String command = extractSingleArgument(args);
290            List<String> results = executeCommandOnAllNodes(command);
291            return new ActionResult(true,
292                String.format("Executed command on %d nodes: %s", results.size(), results));
293        } catch (InterruptedException | ExecutionException e) {
294            logger.log(Level.SEVERE, "executeCommandOnAllNodes failed", e);
295            return new ActionResult(false, "Error: " + e.getMessage());
296        }
297    }
298
299    /**
300     * Checks if accumulator exists.
301     *
302     * @param args the argument string (not used)
303     * @return ActionResult with true if accumulator exists
304     */
305    @Action("hasAccumulator")
306    public ActionResult hasAccumulator(String args) {
307        boolean hasAcc = ((IIActorSystem) this.system()).getIIActor("outputMultiplexer") != null;
308        return new ActionResult(hasAcc, hasAcc ? "Accumulator exists" : "No accumulator");
309    }
310
311    /**
312     * Creates an accumulator (no-op, kept for backward compatibility).
313     *
314     * @param args the argument string (not used)
315     * @return ActionResult with success
316     */
317    @Action("createAccumulator")
318    public ActionResult createAccumulator(String args) {
319        // No-op: MultiplexerAccumulator is now created by RunCLI
320        // This case is kept for backward compatibility with existing workflows
321        return new ActionResult(true, "Accumulator managed by CLI");
322    }
323
324    /**
325     * Gets the summary from the output multiplexer.
326     *
327     * @param args the argument string (not used)
328     * @return ActionResult with the summary
329     */
330    @Action("getAccumulatorSummary")
331    public ActionResult getAccumulatorSummaryAction(String args) {
332        return getAccumulatorSummary();
333    }
334
335    /**
336     * Prints a summary of the current session's verification results.
337     *
338     * @param args the argument string (not used)
339     * @return ActionResult with the summary
340     */
341    @Action("printSessionSummary")
342    public ActionResult printSessionSummaryAction(String args) {
343        return printSessionSummary();
344    }
345
346    /**
347     * Gets the current session ID.
348     *
349     * @param args the argument string (not used)
350     * @return ActionResult with the session ID
351     */
352    @Action("getSessionId")
353    public ActionResult getSessionId(String args) {
354        long sessionId = this.object.getSessionId();
355        if (sessionId < 0) {
356            return new ActionResult(false, "No session ID set");
357        }
358        return new ActionResult(true, String.valueOf(sessionId));
359    }
360
361    /**
362     * Gets the current workflow file path.
363     *
364     * @param args the argument string (not used)
365     * @return ActionResult with the workflow path
366     */
367    @Action("getWorkflowPath")
368    public ActionResult getWorkflowPath(String args) {
369        if (currentWorkflowPath == null) {
370            return new ActionResult(false, "No workflow path set");
371        }
372        return new ActionResult(true, currentWorkflowPath);
373    }
374
375    /**
376     * Does nothing, returns the argument as result.
377     *
378     * @param args the argument string
379     * @return ActionResult with the argument
380     */
381    @Action("doNothing")
382    public ActionResult doNothing(String args) {
383        return new ActionResult(true, args);
384    }
385
386    // ========================================================================
387    // JSON State Output Actions
388    // ========================================================================
389
390    /**
391     * Outputs JSON State at the given path in pretty JSON format via outputMultiplexer.
392     *
393     * @param args the path to output (from JSON array)
394     * @return ActionResult with the formatted JSON
395     */
396    @Action("printJson")
397    public ActionResult printJson(String args) {
398        String path = getFirst(args);
399        String formatted = toStringOfJson(path);
400        sendToMultiplexer(formatted);
401        return new ActionResult(true, formatted);
402    }
403
404    /**
405     * Outputs JSON State at the given path in YAML format via outputMultiplexer.
406     *
407     * @param args the path to output (from JSON array)
408     * @return ActionResult with the formatted YAML
409     */
410    @Action("printYaml")
411    public ActionResult printYaml(String args) {
412        String path = getFirst(args);
413        String formatted = toStringOfYaml(path);
414        sendToMultiplexer(formatted);
415        return new ActionResult(true, formatted);
416    }
417
418    /**
419     * Sends formatted output to the outputMultiplexer, line by line.
420     */
421    private void sendToMultiplexer(String formatted) {
422        IIActorSystem sys = (IIActorSystem) this.system();
423        IIActorRef<?> multiplexer = sys.getIIActor("outputMultiplexer");
424        if (multiplexer == null) {
425            return;
426        }
427
428        String nodeName = this.getName();
429        for (String line : formatted.split("\n")) {
430            JSONObject arg = new JSONObject();
431            arg.put("source", nodeName);
432            arg.put("type", "stdout");
433            arg.put("data", line);
434            multiplexer.callByActionName("add", arg.toString());
435        }
436    }
437
438    // ========================================================================
439    // Helper Methods
440    // ========================================================================
441
442    private int parseMaxIterations(String arg, int defaultValue) {
443        if (arg != null && !arg.isEmpty() && !arg.equals("[]")) {
444            try {
445                JSONArray args = new JSONArray(arg);
446                if (args.length() > 0) {
447                    return args.getInt(0);
448                }
449            } catch (Exception e) {
450                // Use default if parsing fails
451            }
452        }
453        return defaultValue;
454    }
455
456    private String getFirst(String args) {
457        if (args == null || args.isEmpty() || args.equals("[]")) {
458            return "";
459        }
460        try {
461            JSONArray jsonArray = new JSONArray(args);
462            if (jsonArray.length() > 0) {
463                return jsonArray.getString(0);
464            }
465        } catch (Exception e) {
466            // Return empty string if parsing fails
467        }
468        return "";
469    }
470
471    /**
472     * Creates child node actors for all nodes in the specified group.
473     *
474     * <p>This method creates Node POJOs using the NodeGroup's inventory,
475     * wraps each in a NodeInterpreter (for workflow capabilities),
476     * then wraps in a NodeIIAR, and registers them as children of this actor
477     * using the parent-child relationship mechanism.</p>
478     *
479     * <p>Special handling for "local" group: creates a localhost node without
480     * requiring an inventory file. This is useful for development and testing.</p>
481     *
482     * @param groupName the name of the group from the inventory file, or "local" for localhost
483     */
484    private void createNodeActors(String groupName) {
485        // Direct execution (no tell().get() to avoid deadlock when called from workflow)
486        IIActorSystem sys = (IIActorSystem) this.system();
487        NodeGroupInterpreter nodeGroupInterpreter = this.object;
488
489        // Create Node POJOs for the group
490        // Special handling for "local" group: create localhost node without inventory
491        List<Node> nodes;
492        if ("local".equals(groupName)) {
493            nodes = nodeGroupInterpreter.createLocalNode();
494        } else {
495            nodes = nodeGroupInterpreter.createNodesForGroup(groupName);
496        }
497
498        // Create child actors for each node
499        for (Node node : nodes) {
500            String nodeName = "node-" + node.getHostname();
501
502            // Wrap Node in NodeInterpreter to add workflow capabilities
503            NodeInterpreter nodeInterpreter = new NodeInterpreter(node, sys);
504
505            // Propagate workflowBaseDir to child interpreter
506            if (nodeGroupInterpreter.getWorkflowBaseDir() != null) {
507                nodeInterpreter.setWorkflowBaseDir(nodeGroupInterpreter.getWorkflowBaseDir());
508            }
509
510            // Propagate overlayDir to child interpreter
511            if (nodeGroupInterpreter.getOverlayDir() != null) {
512                nodeInterpreter.setOverlayDir(nodeGroupInterpreter.getOverlayDir());
513            }
514
515            // Propagate accumulator to child interpreter
516            if (nodeGroupInterpreter.getAccumulator() != null) {
517                nodeInterpreter.setAccumulator(nodeGroupInterpreter.getAccumulator());
518            }
519
520            // Create child actor using ActorRef.createChild()
521            // This establishes parent-child relationship
522            this.createChild(nodeName, nodeInterpreter);
523
524            // Also wrap in NodeIIAR and add to system for workflow execution
525            NodeIIAR nodeActor = new NodeIIAR(nodeName, nodeInterpreter, sys);
526            sys.addIIActor(nodeActor);
527
528            logger.fine(String.format("Created child node actor: %s", nodeName));
529        }
530        logger.info(String.format("Created %d node actors for group '%s'", nodes.size(), groupName));
531    }
532
533    /**
534     * Applies an action to child actors matching a wildcard pattern.
535     *
536     * <p>This method parses an action definition JSON and executes the specified
537     * method on all child actors whose names match the pattern.</p>
538     *
539     * <p>Action definition format:</p>
540     * <pre>{@code
541     * {
542     *   "actor": "node-*",           // Wildcard pattern for actor names
543     *   "method": "executeCommand",  // Method to call
544     *   "arguments": ["ls -la"]      // Arguments (optional)
545     * }
546     * }</pre>
547     *
548     * <p>Supported wildcard patterns:</p>
549     * <ul>
550     * <li>{@code *} - Matches all child actors</li>
551     * <li>{@code node-*} - Matches actors starting with "node-"</li>
552     * <li>{@code *-web} - Matches actors ending with "-web"</li>
553     * </ul>
554     *
555     * @param actionDef JSON string defining the action to apply
556     * @return ActionResult indicating success or failure
557     */
558    /**
559     * Applies an action to multiple actors matching a pattern in parallel.
560     *
561     * <p>This method executes the specified action on all matching actors
562     * asynchronously and in parallel, then waits for all executions to complete
563     * before returning. This provides significant performance benefits when
564     * executing actions on many nodes.</p>
565     *
566     * @param actionDef JSON string defining the action to apply
567     * @return ActionResult indicating success or failure
568     */
569    private ActionResult apply(String actionDef) {
570        try {
571            JSONObject action = new JSONObject(actionDef);
572            String actorPattern = action.getString("actor");
573            String method = action.getString("method");
574            JSONArray argsArray = action.optJSONArray("arguments");
575            String args = argsArray != null ? argsArray.toString() : "[]";
576
577            // Find matching child actors
578            List<IIActorRef<?>> matchedActors = findMatchingChildActors(actorPattern);
579
580            if (matchedActors.isEmpty()) {
581                return new ActionResult(false, "No actors matched pattern: " + actorPattern);
582            }
583
584            logger.info(String.format("Applying method '%s' to %d actors matching '%s' (async parallel)",
585                method, matchedActors.size(), actorPattern));
586
587            // Thread-safe collections for gathering results
588            AtomicInteger successCount = new AtomicInteger(0);
589            Map<String, String> failures = new ConcurrentHashMap<>();
590            DistributedLogStore logStore = this.object.getLogStore();
591            long sessionId = this.object.getSessionId();
592
593            // Create async tasks for all actors
594            List<CompletableFuture<Void>> futures = new ArrayList<>();
595
596            for (IIActorRef<?> actor : matchedActors) {
597                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
598                    try {
599                        ActionResult result = actor.callByActionName(method, args);
600                        if (!result.isSuccess()) {
601                            failures.put(actor.getName(), result.getResult());
602                            logger.warning(String.format("Failed on %s: %s", actor.getName(), result.getResult()));
603                            // Record node failure in log store
604                            if (logStore != null && sessionId >= 0) {
605                                logStore.markNodeFailed(sessionId, actor.getName(), result.getResult());
606                            }
607                        } else {
608                            successCount.incrementAndGet();
609                            logger.fine(String.format("Applied to %s: %s", actor.getName(), result.getResult()));
610                            // Record node success in log store
611                            if (logStore != null && sessionId >= 0) {
612                                logStore.markNodeSuccess(sessionId, actor.getName());
613                            }
614                        }
615                    } catch (Exception e) {
616                        failures.put(actor.getName(), e.getMessage());
617                        logger.log(Level.WARNING, "Exception on " + actor.getName(), e);
618                    }
619                });
620                futures.add(future);
621            }
622
623            // Wait for all async tasks to complete
624            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
625
626            if (failures.isEmpty()) {
627                return new ActionResult(true,
628                    String.format("Applied to %d actors", successCount.get()));
629            } else {
630                List<String> failureMessages = new ArrayList<>();
631                failures.forEach((name, msg) -> failureMessages.add(name + ": " + msg));
632                return new ActionResult(false,
633                    String.format("Applied to %d/%d actors. Failures: %s",
634                        successCount.get(), matchedActors.size(), String.join("; ", failureMessages)));
635            }
636
637        } catch (Exception e) {
638            logger.log(Level.SEVERE, "Error in apply: " + actionDef, e);
639            return new ActionResult(false, "Error: " + e.getMessage());
640        }
641    }
642
643    /**
644     * Finds child actors matching a wildcard pattern.
645     *
646     * @param pattern the wildcard pattern (e.g., "node-*", "*-web", "*")
647     * @return list of matching child actors
648     */
649    private List<IIActorRef<?>> findMatchingChildActors(String pattern) {
650        List<IIActorRef<?>> matched = new ArrayList<>();
651        IIActorSystem system = (IIActorSystem) this.system();
652
653        if (system == null) {
654            return matched;
655        }
656
657        List<String> childNames = new ArrayList<>(this.getNamesOfChildren());
658
659        // Exact match (no wildcard)
660        if (!pattern.contains("*")) {
661            if (childNames.contains(pattern)) {
662                IIActorRef<?> actor = system.getIIActor(pattern);
663                if (actor != null) {
664                    matched.add(actor);
665                }
666            }
667            return matched;
668        }
669
670        // Convert wildcard to regex
671        String regex = pattern
672            .replace(".", "\\.")
673            .replace("*", ".*");
674        Pattern compiled = Pattern.compile(regex);
675
676        for (String childName : childNames) {
677            if (compiled.matcher(childName).matches()) {
678                IIActorRef<?> actor = system.getIIActor(childName);
679                if (actor != null) {
680                    matched.add(actor);
681                }
682            }
683        }
684
685        return matched;
686    }
687
688    /**
689     * Executes a single command on all child node actors.
690     *
691     * @param command the command to execute
692     * @return list of results from each node
693     * @throws ExecutionException if command execution fails
694     * @throws InterruptedException if the operation is interrupted
695     */
696    private List<String> executeCommandOnAllNodes(String command)
697            throws ExecutionException, InterruptedException {
698
699        IIActorSystem system = (IIActorSystem) this.system();
700        List<String> results = new ArrayList<>();
701
702        // Get all child node names
703        List<String> childNames = new ArrayList<>(this.getNamesOfChildren());
704
705        logger.info(String.format("Executing command on %d nodes: %s", childNames.size(), command));
706
707        // Execute on each child node
708        for (String childName : childNames) {
709            IIActorRef<?> actorRef = system.getIIActor(childName);
710            if (actorRef == null || !(actorRef instanceof NodeIIAR)) {
711                logger.warning(String.format("Child node actor not found or wrong type: %s", childName));
712                continue;
713            }
714            NodeIIAR nodeActor = (NodeIIAR) actorRef;
715
716            // Execute the command
717            JSONArray commandArgs = new JSONArray();
718            commandArgs.put(command);
719            ActionResult result = nodeActor.callByActionName("executeCommand", commandArgs.toString());
720
721            results.add(String.format("%s: %s", childName, result.getResult()));
722        }
723
724        return results;
725    }
726
727    /**
728     * Extracts a single argument from JSON array format.
729     *
730     * @param arg the JSON array argument string
731     * @return the extracted argument
732     */
733    private String extractSingleArgument(String arg) {
734        try {
735            JSONArray jsonArray = new JSONArray(arg);
736            if (jsonArray.length() == 0) {
737                throw new IllegalArgumentException("Arguments cannot be empty");
738            }
739            return jsonArray.getString(0);
740        } catch (Exception e) {
741            throw new IllegalArgumentException(
742                "Invalid argument format. Expected JSON array: " + arg, e);
743        }
744    }
745
746    /**
747     * Gets the summary from the output multiplexer.
748     *
749     * <p>Retrieves the multiplexer actor from ActorSystem by name ("outputMultiplexer")
750     * and calls its getSummary action.</p>
751     *
752     * @return ActionResult with the summary or error
753     */
754    private ActionResult getAccumulatorSummary() {
755        IIActorSystem sys = (IIActorSystem) this.system();
756        IIActorRef<?> multiplexer = sys.getIIActor("outputMultiplexer");
757        if (multiplexer == null) {
758            return new ActionResult(false, "No output multiplexer registered");
759        }
760        ActionResult result = multiplexer.callByActionName("getSummary", "");
761        return result;
762    }
763
764    /**
765     * Prints a summary of the current session's verification results.
766     *
767     * <p>Groups results by label (step) and displays a formatted table.</p>
768     *
769     * @return ActionResult with success status and summary text
770     */
771    private ActionResult printSessionSummary() {
772        DistributedLogStore logStore = this.object.getLogStore();
773        long sessionId = this.object.getSessionId();
774
775        if (logStore == null || sessionId < 0) {
776            String msg = "Log store not available";
777            System.out.println(msg);
778            return new ActionResult(false, msg);
779        }
780
781        // Get all logs for this session
782        List<com.scivicslab.actoriac.log.LogEntry> logs = logStore.getLogsByLevel(sessionId,
783            com.scivicslab.actoriac.log.LogLevel.DEBUG);
784
785        // Group logs by label and count results
786        java.util.Map<String, VerifyResult> resultsByLabel = new java.util.LinkedHashMap<>();
787
788        for (com.scivicslab.actoriac.log.LogEntry entry : logs) {
789            String message = entry.getMessage();
790            String label = entry.getLabel();
791            if (message == null) continue;
792
793            // Extract label from the message if it contains step info
794            // Format: "- states: [...]\n  label: xxx\n..."
795            if (label != null && label.contains("label:")) {
796                int idx = label.indexOf("label:");
797                if (idx >= 0) {
798                    String rest = label.substring(idx + 11).trim();
799                    int end = rest.indexOf('\n');
800                    label = end > 0 ? rest.substring(0, end).trim() : rest.trim();
801                }
802            }
803
804            // Skip non-verify steps
805            if (label == null || !label.startsWith("verify-")) {
806                continue;
807            }
808
809            VerifyResult result = resultsByLabel.computeIfAbsent(label, k -> new VerifyResult());
810
811            // Count occurrences in message
812            result.okCount += countOccurrences(message, "[OK]");
813            result.warnCount += countOccurrences(message, "[WARN]");
814            result.errorCount += countOccurrences(message, "[ERROR]");
815            result.infoCount += countOccurrences(message, "[INFO]");
816
817            // Extract special info (like document count, cluster health)
818            extractSpecialInfo(message, result);
819        }
820
821        // Build summary output
822        StringBuilder sb = new StringBuilder();
823        sb.append("\n");
824        sb.append("============================================================\n");
825        sb.append("                 VERIFICATION SUMMARY\n");
826        sb.append("============================================================\n");
827        sb.append("\n");
828        sb.append(String.format("| %-35s | %-20s |\n", "Item", "Status"));
829        sb.append("|-------------------------------------|----------------------|\n");
830
831        // Mapping from labels to display names and aggregation
832        String[][] mappings = {
833            {"verify-repos", "Document repositories"},
834            {"verify-utility-cli", "Utility-cli"},
835            {"verify-utility-sau3", "Utility-sau3"},
836            {"verify-builds", "Docusaurus builds"},
837            {"verify-public-html", "public_html deploy"},
838            {"verify-apache", "Apache2 + UserDir"},
839            {"verify-opensearch-install", "OpenSearch install"},
840            {"verify-opensearch-running", "OpenSearch status"},
841            {"verify-docusearch-build", "quarkus-docusearch build"},
842            {"verify-docusearch-running", "quarkus-docusearch server"},
843            {"verify-search-index", "Search index"},
844            {"verify-web-access", "Web access"},
845        };
846
847        int totalOk = 0, totalWarn = 0, totalError = 0;
848        int executedChecks = 0;
849        List<String> errorDetails = new ArrayList<>();
850        List<String> warnDetails = new ArrayList<>();
851
852        for (String[] mapping : mappings) {
853            String label = mapping[0];
854            String displayName = mapping[1];
855            VerifyResult result = resultsByLabel.get(label);
856
857            if (result == null) {
858                sb.append(String.format("| %-35s | %-20s |\n", displayName, "SKIP"));
859                continue;
860            }
861            executedChecks++;
862
863            totalOk += result.okCount;
864            totalWarn += result.warnCount;
865            totalError += result.errorCount;
866
867            String status = formatStatus(result);
868            sb.append(String.format("| %-35s | %-20s |\n", displayName, status));
869
870            // Collect error/warning details
871            if (result.errorCount > 0) {
872                errorDetails.add(displayName + ": " + result.errorCount + " error(s)");
873            }
874            if (result.warnCount > 0) {
875                warnDetails.add(displayName + ": " + result.warnCount + " warning(s)");
876            }
877        }
878
879        sb.append("|-------------------------------------|----------------------|\n");
880        sb.append(String.format("| %-35s | %d OK, %d WARN, %d ERR |\n",
881            "TOTAL", totalOk, totalWarn, totalError));
882        sb.append("============================================================\n");
883
884        // Show error details if any
885        if (!errorDetails.isEmpty()) {
886            sb.append("\n--- Errors ---\n");
887            for (String detail : errorDetails) {
888                sb.append("  * ").append(detail).append("\n");
889            }
890        }
891
892        // Show warning details if any
893        if (!warnDetails.isEmpty()) {
894            sb.append("\n--- Warnings ---\n");
895            for (String detail : warnDetails) {
896                sb.append("  * ").append(detail).append("\n");
897            }
898        }
899
900        sb.append("\n");
901        if (executedChecks == 0) {
902            sb.append("No verification checks were executed.\n");
903        } else if (totalError == 0 && totalWarn == 0) {
904            sb.append("All checks passed!\n");
905        } else if (totalError > 0) {
906            sb.append("To fix issues, run:\n");
907            sb.append("  ./actor_iac.java --dir ./docu-search --workflow main-setup\n");
908        }
909
910        String summary = sb.toString();
911        System.out.println(summary);
912        return new ActionResult(true, summary);
913    }
914
915    /**
916     * Formats the status string for a verification result.
917     */
918    private String formatStatus(VerifyResult result) {
919        if (result.errorCount > 0) {
920            if (result.okCount > 0) {
921                return String.format("%d OK, %d ERROR", result.okCount, result.errorCount);
922            }
923            return "ERROR";
924        }
925        if (result.warnCount > 0) {
926            if (result.okCount > 0) {
927                return String.format("%d OK, %d WARN", result.okCount, result.warnCount);
928            }
929            return "WARN";
930        }
931        if (result.okCount > 0) {
932            String extra = result.extraInfo != null ? " " + result.extraInfo : "";
933            return result.okCount + " OK" + extra;
934        }
935        return "OK";
936    }
937
938    /**
939     * Extracts special information from log messages (like document count, cluster health).
940     */
941    private void extractSpecialInfo(String message, VerifyResult result) {
942        // Extract document count from search index
943        if (message.contains("documents")) {
944            java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d+)\\s+documents").matcher(message);
945            if (m.find()) {
946                result.extraInfo = "(" + m.group(1) + " docs)";
947            }
948        }
949        // Extract cluster health
950        if (message.contains("Cluster health:")) {
951            java.util.regex.Matcher m = java.util.regex.Pattern.compile("Cluster health:\\s*(\\w+)").matcher(message);
952            if (m.find()) {
953                result.extraInfo = "(" + m.group(1) + ")";
954            }
955        }
956        // Extract web access count
957        if (message.contains("Accessible at")) {
958            // Count from "X / Y" pattern is handled by OK count
959        }
960    }
961
962    /**
963     * Counts occurrences of a substring in a string.
964     */
965    private int countOccurrences(String text, String sub) {
966        int count = 0;
967        int idx = 0;
968        while ((idx = text.indexOf(sub, idx)) != -1) {
969            count++;
970            idx += sub.length();
971        }
972        return count;
973    }
974
975    /**
976     * Helper class to hold verification results for a step.
977     */
978    private static class VerifyResult {
979        int okCount = 0;
980        int warnCount = 0;
981        int errorCount = 0;
982        int infoCount = 0;
983        String extraInfo = null;
984    }
985
986}