001/*
002 * Copyright 2025 devteam@scivics-lab.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.io.File;
021import java.io.FileInputStream;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.concurrent.ExecutionException;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.json.JSONArray;
030import org.json.JSONObject;
031
032import com.github.lalyos.jfiglet.FigletFont;
033import com.scivicslab.pojoactor.core.ActionResult;
034import com.scivicslab.pojoactor.workflow.IIActorRef;
035import com.scivicslab.pojoactor.workflow.IIActorSystem;
036
037/**
038 * Interpreter-interfaced actor reference for {@link NodeInterpreter} instances.
039 *
040 * <p>This class provides a concrete implementation of {@link IIActorRef}
041 * specifically for {@link NodeInterpreter} objects. It handles action invocations
042 * by name, supporting both workflow execution actions (inherited from Interpreter)
043 * and infrastructure actions (SSH command execution).</p>
044 *
045 * <p><strong>Supported actions:</strong></p>
046 * <p><em>Workflow actions (from Interpreter):</em></p>
047 * <ul>
048 *   <li>{@code execCode} - Executes the loaded workflow code</li>
049 *   <li>{@code readYaml} - Reads a YAML workflow definition from a file path</li>
050 *   <li>{@code readJson} - Reads a JSON workflow definition from a file path</li>
051 *   <li>{@code readXml} - Reads an XML workflow definition from a file path</li>
052 *   <li>{@code reset} - Resets the interpreter state</li>
053 * </ul>
054 * <p><em>Infrastructure actions (Node-specific):</em></p>
055 * <ul>
056 *   <li>{@code executeCommand} - Executes a command and reports to accumulator (default)</li>
057 *   <li>{@code executeCommandQuiet} - Executes a command without reporting</li>
058 *   <li>{@code executeSudoCommand} - Executes sudo command and reports to accumulator (default)</li>
059 *   <li>{@code executeSudoCommandQuiet} - Executes sudo command without reporting</li>
060 * </ul>
061 *
062 * <p><strong>Example YAML Workflow:</strong></p>
063 * <pre>{@code
064 * name: deploy-application
065 * steps:
066 *   - states: ["0", "1"]
067 *     actions:
068 *       - actor: this
069 *         method: executeCommand
070 *         arguments:
071 *           - "apt-get update"
072 *   - states: ["1", "end"]
073 *     actions:
074 *       - actor: this
075 *         method: executeCommand
076 *         arguments:
077 *           - "ls -la"
078 * }</pre>
079 *
080 * @author devteam@scivics-lab.com
081 */
082public class NodeIIAR extends IIActorRef<NodeInterpreter> {
083
084    Logger logger = null;
085
086    /**
087     * Constructs a new NodeIIAR with the specified actor name and node interpreter object.
088     *
089     * @param actorName the name of this actor
090     * @param object the {@link NodeInterpreter} instance managed by this actor reference
091     */
092    public NodeIIAR(String actorName, NodeInterpreter object) {
093        super(actorName, object);
094        logger = Logger.getLogger(actorName);
095    }
096
097    /**
098     * Constructs a new NodeIIAR with the specified actor name, node interpreter object,
099     * and actor system.
100     *
101     * @param actorName the name of this actor
102     * @param object the {@link NodeInterpreter} instance managed by this actor reference
103     * @param system the actor system managing this actor
104     */
105    public NodeIIAR(String actorName, NodeInterpreter object, IIActorSystem system) {
106        super(actorName, object, system);
107        logger = Logger.getLogger(actorName);
108
109        // Set the selfActorRef in the Interpreter (NodeInterpreter extends Interpreter)
110        object.setSelfActorRef(this);
111    }
112
113    /**
114     * Invokes an action on the node by name with the given arguments.
115     *
116     * <p>This method dispatches to specialized handler methods based on the action type:</p>
117     * <ul>
118     *   <li>Workflow actions: {@link #handleWorkflowAction}</li>
119     *   <li>Command execution actions: {@link #handleCommandAction}</li>
120     *   <li>Utility actions: {@link #handleUtilityAction}</li>
121     * </ul>
122     *
123     * @param actionName the name of the action to execute
124     * @param arg the argument string (file path for read operations, JSON array for commands)
125     * @return an {@link ActionResult} indicating success or failure with a message
126     */
127    @Override
128    public ActionResult callByActionName(String actionName, String arg) {
129        logger.fine(String.format("actionName = %s, args = %s", actionName, arg));
130
131        try {
132            // Workflow execution actions (from Interpreter)
133            ActionResult workflowResult = handleWorkflowAction(actionName, arg);
134            if (workflowResult != null) {
135                return workflowResult;
136            }
137
138            // Node-specific actions (SSH command execution)
139            ActionResult commandResult = handleCommandAction(actionName, arg);
140            if (commandResult != null) {
141                return commandResult;
142            }
143
144            // Utility actions
145            ActionResult utilityResult = handleUtilityAction(actionName, arg);
146            if (utilityResult != null) {
147                return utilityResult;
148            }
149
150            // Document workflow actions
151            ActionResult documentResult = handleDocumentAction(actionName, arg);
152            if (documentResult != null) {
153                return documentResult;
154            }
155
156            // Unknown action
157            logger.log(Level.SEVERE, String.format("Unknown action: actorName = %s, action = %s, arg = %s",
158                    this.getName(), actionName, arg));
159            return new ActionResult(false, "Unknown action: " + actionName);
160
161        } catch (InterruptedException e) {
162            String message = "Interrupted: " + e.getMessage();
163            logger.warning(String.format("%s: %s", this.getName(), message));
164            return new ActionResult(false, message);
165        } catch (ExecutionException e) {
166            String message = extractRootCauseMessage(e);
167            logger.warning(String.format("%s: %s", this.getName(), message));
168            return new ActionResult(false, message);
169        }
170    }
171
172    /**
173     * Handles workflow-related actions (from Interpreter).
174     *
175     * @param actionName the action name
176     * @param arg the argument string
177     * @return ActionResult if handled, null if not a workflow action
178     */
179    private ActionResult handleWorkflowAction(String actionName, String arg)
180            throws InterruptedException, ExecutionException {
181
182        switch (actionName) {
183            case "execCode":
184                return this.ask(n -> n.execCode()).get();
185
186            case "readYaml":
187                return handleReadYaml(arg);
188
189            case "readJson":
190                return handleReadJson(arg);
191
192            case "readXml":
193                return handleReadXml(arg);
194
195            case "reset":
196                this.tell(n -> n.reset()).get();
197                return new ActionResult(true, "Interpreter reset successfully");
198
199            case "runUntilEnd":
200                int maxIterations = parseMaxIterations(arg, 10000);
201                return this.ask(n -> n.runUntilEnd(maxIterations)).get();
202
203            case "call":
204                JSONArray callArgs = new JSONArray(arg);
205                String callWorkflowFile = callArgs.getString(0);
206                return this.ask(n -> n.call(callWorkflowFile)).get();
207
208            case "runWorkflow":
209                JSONArray runArgs = new JSONArray(arg);
210                String runWorkflowFile = runArgs.getString(0);
211                int runMaxIterations = runArgs.length() > 1 ? runArgs.getInt(1) : 10000;
212                logger.fine(String.format("Running workflow: %s (maxIterations=%d)", runWorkflowFile, runMaxIterations));
213                ActionResult result = this.object.runWorkflow(runWorkflowFile, runMaxIterations);
214                logger.fine(String.format("Workflow completed: success=%s, result=%s", result.isSuccess(), result.getResult()));
215                return result;
216
217            case "apply":
218                return this.ask(n -> n.apply(arg)).get();
219
220            default:
221                return null; // Not a workflow action
222        }
223    }
224
225    /**
226     * Handles SSH command execution actions.
227     *
228     * @param actionName the action name
229     * @param arg the argument string
230     * @return ActionResult if handled, null if not a command action
231     */
232    private ActionResult handleCommandAction(String actionName, String arg)
233            throws InterruptedException, ExecutionException {
234
235        switch (actionName) {
236            case "executeCommandQuiet":
237                return executeCommandQuiet(arg);
238
239            case "executeSudoCommandQuiet":
240                return executeSudoCommandQuiet(arg);
241
242            case "executeCommand":
243                return executeCommandWithReport(arg);
244
245            case "executeSudoCommand":
246                return executeSudoCommandWithReport(arg);
247
248            default:
249                return null; // Not a command action
250        }
251    }
252
253    /**
254     * Handles utility actions (sleep, print, doNothing).
255     *
256     * @param actionName the action name
257     * @param arg the argument string
258     * @return ActionResult if handled, null if not a utility action
259     */
260    private ActionResult handleUtilityAction(String actionName, String arg) {
261        switch (actionName) {
262            case "sleep":
263                try {
264                    long millis = Long.parseLong(arg);
265                    Thread.sleep(millis);
266                    return new ActionResult(true, "Slept for " + millis + "ms");
267                } catch (NumberFormatException e) {
268                    logger.log(Level.SEVERE, String.format("Invalid sleep duration: %s", arg), e);
269                    return new ActionResult(false, "Invalid sleep duration: " + arg);
270                } catch (InterruptedException e) {
271                    return new ActionResult(false, "Sleep interrupted: " + e.getMessage());
272                }
273
274            case "print":
275                System.out.println(arg);
276                return new ActionResult(true, "Printed: " + arg);
277
278            case "doNothing":
279                return new ActionResult(true, arg);
280
281            default:
282                return null; // Not a utility action
283        }
284    }
285
286    /**
287     * Handles document workflow actions (detect, clone, build, deploy).
288     *
289     * @param actionName the action name
290     * @param arg the argument string
291     * @return ActionResult if handled, null if not a document action
292     */
293    private ActionResult handleDocumentAction(String actionName, String arg)
294            throws InterruptedException, ExecutionException {
295
296        switch (actionName) {
297            case "detectDocumentChanges":
298                return this.ask(n -> {
299                    try {
300                        String docListPath = extractCommandFromArgs(arg);
301                        return n.detectDocumentChanges(docListPath);
302                    } catch (IOException e) {
303                        return new ActionResult(false, "Error detecting changes: " + e.getMessage());
304                    }
305                }).get();
306
307            case "cloneChangedDocuments":
308                return this.ask(n -> {
309                    try {
310                        String docListPath = extractCommandFromArgs(arg);
311                        return n.cloneChangedDocuments(docListPath);
312                    } catch (IOException e) {
313                        return new ActionResult(false, "Error cloning documents: " + e.getMessage());
314                    }
315                }).get();
316
317            case "buildChangedDocuments":
318                return this.ask(n -> {
319                    try {
320                        String docListPath = extractCommandFromArgs(arg);
321                        return n.buildChangedDocuments(docListPath);
322                    } catch (IOException e) {
323                        return new ActionResult(false, "Error building documents: " + e.getMessage());
324                    }
325                }).get();
326
327            case "deployChangedDocuments":
328                return this.ask(n -> {
329                    try {
330                        String docListPath = extractCommandFromArgs(arg);
331                        return n.deployChangedDocuments(docListPath);
332                    } catch (IOException e) {
333                        return new ActionResult(false, "Error deploying documents: " + e.getMessage());
334                    }
335                }).get();
336
337            default:
338                return null; // Not a document action
339        }
340    }
341
342    // --- Helper methods for workflow actions ---
343
344    private ActionResult handleReadYaml(String arg) throws InterruptedException, ExecutionException {
345        try {
346            String overlayPath = this.object.getOverlayDir();
347            if (overlayPath != null) {
348                java.nio.file.Path yamlPath = java.nio.file.Path.of(arg);
349                java.nio.file.Path overlayDir = java.nio.file.Path.of(overlayPath);
350                this.tell(n -> {
351                    try {
352                        n.readYaml(yamlPath, overlayDir);
353                    } catch (IOException e) {
354                        throw new RuntimeException(e);
355                    }
356                }).get();
357                return new ActionResult(true, "YAML loaded with overlay: " + overlayPath);
358            } else {
359                try (InputStream input = new FileInputStream(new File(arg))) {
360                    this.tell(n -> n.readYaml(input)).get();
361                    return new ActionResult(true, "YAML loaded successfully");
362                }
363            }
364        } catch (FileNotFoundException e) {
365            logger.log(Level.SEVERE, String.format("file not found: %s", arg), e);
366            return new ActionResult(false, "File not found: " + arg);
367        } catch (IOException e) {
368            logger.log(Level.SEVERE, String.format("IOException: %s", arg), e);
369            return new ActionResult(false, "IO error: " + arg);
370        } catch (RuntimeException e) {
371            if (e.getCause() instanceof IOException) {
372                logger.log(Level.SEVERE, String.format("IOException: %s", arg), e.getCause());
373                return new ActionResult(false, "IO error: " + arg);
374            }
375            throw e;
376        }
377    }
378
379    private ActionResult handleReadJson(String arg) throws InterruptedException, ExecutionException {
380        try (InputStream input = new FileInputStream(new File(arg))) {
381            this.tell(n -> {
382                try {
383                    n.readJson(input);
384                } catch (IOException e) {
385                    throw new RuntimeException(e);
386                }
387            }).get();
388            return new ActionResult(true, "JSON loaded successfully");
389        } catch (FileNotFoundException e) {
390            logger.log(Level.SEVERE, String.format("file not found: %s", arg), e);
391            return new ActionResult(false, "File not found: " + arg);
392        } catch (IOException e) {
393            logger.log(Level.SEVERE, String.format("IOException: %s", arg), e);
394            return new ActionResult(false, "IO error: " + arg);
395        }
396    }
397
398    private ActionResult handleReadXml(String arg) throws InterruptedException, ExecutionException {
399        try (InputStream input = new FileInputStream(new File(arg))) {
400            this.tell(n -> {
401                try {
402                    n.readXml(input);
403                } catch (Exception e) {
404                    throw new RuntimeException(e);
405                }
406            }).get();
407            return new ActionResult(true, "XML loaded successfully");
408        } catch (FileNotFoundException e) {
409            logger.log(Level.SEVERE, String.format("file not found: %s", arg), e);
410            return new ActionResult(false, "File not found: " + arg);
411        } catch (Exception e) {
412            logger.log(Level.SEVERE, String.format("Exception: %s", arg), e);
413            return new ActionResult(false, "Error: " + arg);
414        }
415    }
416
417    private int parseMaxIterations(String arg, int defaultValue) {
418        if (arg != null && !arg.isEmpty() && !arg.equals("[]")) {
419            try {
420                JSONArray args = new JSONArray(arg);
421                if (args.length() > 0) {
422                    return args.getInt(0);
423                }
424            } catch (Exception e) {
425                // Use default if parsing fails
426            }
427        }
428        return defaultValue;
429    }
430
431    // --- Helper methods for command actions ---
432
433    private ActionResult executeCommandQuiet(String arg) throws InterruptedException, ExecutionException {
434        String command = extractCommandFromArgs(arg);
435        Node.CommandResult result = this.ask(n -> {
436            try {
437                return n.executeCommand(command);
438            } catch (IOException e) {
439                throw new RuntimeException(e);
440            }
441        }).get();
442
443        return new ActionResult(result.isSuccess(),
444            String.format("exitCode=%d, stdout='%s', stderr='%s'",
445                result.getExitCode(), result.getStdout(), result.getStderr()));
446    }
447
448    private ActionResult executeSudoCommandQuiet(String arg) throws InterruptedException, ExecutionException {
449        String command = extractCommandFromArgs(arg);
450        Node.CommandResult result = this.ask(n -> {
451            try {
452                return n.executeSudoCommand(command);
453            } catch (IOException e) {
454                throw new RuntimeException(e);
455            }
456        }).get();
457
458        return new ActionResult(result.isSuccess(),
459            String.format("exitCode=%d, stdout='%s', stderr='%s'",
460                result.getExitCode(), result.getStdout(), result.getStderr()));
461    }
462
463    private ActionResult executeCommandWithReport(String arg) throws InterruptedException, ExecutionException {
464        String command = extractCommandFromArgs(arg);
465        Node.CommandResult result = this.ask(n -> {
466            try {
467                return n.executeCommand(command);
468            } catch (IOException e) {
469                throw new RuntimeException(e);
470            }
471        }).get();
472
473        reportToAccumulator(result);
474        return new ActionResult(result.isSuccess(), combineOutput(result));
475    }
476
477    private ActionResult executeSudoCommandWithReport(String arg) throws InterruptedException, ExecutionException {
478        String command = extractCommandFromArgs(arg);
479        Node.CommandResult result = this.ask(n -> {
480            try {
481                return n.executeSudoCommand(command);
482            } catch (IOException e) {
483                throw new RuntimeException(e);
484            }
485        }).get();
486
487        reportToAccumulator(result);
488        return new ActionResult(result.isSuccess(), combineOutput(result));
489    }
490
491    /**
492     * Reports command result to the accumulator actor if available.
493     */
494    private void reportToAccumulator(Node.CommandResult result) {
495        IIActorSystem sys = (IIActorSystem) this.system();
496        IIActorRef<?> accumulator = sys.getIIActor("accumulator");
497        if (accumulator != null) {
498            JSONObject reportArg = new JSONObject();
499            reportArg.put("source", this.getName());
500            reportArg.put("type", this.object.getCurrentVertexYaml());
501            reportArg.put("data", combineOutput(result));
502            accumulator.callByActionName("add", reportArg.toString());
503        }
504    }
505
506    /**
507     * Combines stdout and stderr into a single output string.
508     */
509    private String combineOutput(Node.CommandResult result) {
510        String output = result.getStdout().trim();
511        String stderr = result.getStderr().trim();
512        if (!stderr.isEmpty()) {
513            output = output.isEmpty() ? stderr : output + "\n[stderr]\n" + stderr;
514        }
515        return output;
516    }
517
518    /**
519     * Extracts the root cause message from an ExecutionException.
520     */
521    private String extractRootCauseMessage(ExecutionException e) {
522        Throwable cause = e.getCause();
523        while (cause != null && cause.getCause() != null) {
524            cause = cause.getCause();
525        }
526        return cause != null ? cause.getMessage() : e.getMessage();
527    }
528
529    /**
530     * Extracts a command string from JSON array arguments.
531     *
532     * <p>Expects arguments in the format: {@code ["command string"]}</p>
533     *
534     * @param arg the JSON array argument string
535     * @return the extracted command string
536     * @throws IllegalArgumentException if the argument format is invalid
537     */
538    private String extractCommandFromArgs(String arg) {
539        try {
540            JSONArray jsonArray = new JSONArray(arg);
541            if (jsonArray.length() == 0) {
542                throw new IllegalArgumentException("Command arguments cannot be empty");
543            }
544            return jsonArray.getString(0);
545        } catch (Exception e) {
546            throw new IllegalArgumentException(
547                "Invalid command argument format. Expected JSON array with command string: " + arg, e);
548        }
549    }
550
551}