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.List;
021import java.util.concurrent.ExecutorService;
022
023import org.json.JSONObject;
024
025import com.scivicslab.actoriac.log.DistributedLogStore;
026import com.scivicslab.actoriac.log.LogLevel;
027import com.scivicslab.pojoactor.core.ActionResult;
028import com.scivicslab.pojoactor.core.ActorRef;
029import com.scivicslab.pojoactor.workflow.IIActorRef;
030import com.scivicslab.pojoactor.workflow.IIActorSystem;
031import com.scivicslab.pojoactor.workflow.Interpreter;
032import com.scivicslab.pojoactor.workflow.Transition;
033
034/**
035 * Level 3 wrapper that adds workflow capabilities to a NodeGroup POJO.
036 *
037 * <p>This class extends {@link Interpreter} to provide workflow execution
038 * capabilities while delegating node group operations to a wrapped {@link NodeGroup} instance.</p>
039 *
040 * <p>This follows the same three-level architecture as NodeInterpreter:</p>
041 * <ul>
042 * <li><strong>Level 1 (POJO):</strong> {@link NodeGroup} - pure POJO with inventory management</li>
043 * <li><strong>Level 2 (Actor):</strong> ActorRef&lt;NodeGroup&gt; - actor wrapper for concurrent execution</li>
044 * <li><strong>Level 3 (Workflow):</strong> NodeGroupInterpreter - workflow capabilities + IIActorRef wrapper</li>
045 * </ul>
046 *
047 * <p><strong>Design principle:</strong> NodeGroup remains a pure POJO, independent of ActorSystem.
048 * NodeGroupInterpreter wraps NodeGroup to add workflow capabilities without modifying the NodeGroup class.</p>
049 *
050 * @author devteam@scivicslab.com
051 */
052public class NodeGroupInterpreter extends Interpreter {
053
054    /**
055     * The wrapped NodeGroup POJO that handles inventory and node creation.
056     */
057    private final NodeGroup nodeGroup;
058
059    /**
060     * The overlay directory path for YAML overlay feature.
061     * When set, workflows are loaded with overlay applied.
062     */
063    private String overlayDir;
064
065    /**
066     * Verbose output flag.
067     * When true, displays full YAML for each transition instead of truncated version.
068     */
069    private boolean verbose = false;
070
071    /**
072     * IaCStreamingAccumulator for displaying workflow steps with cowsay ASCII art.
073     * When set, workflow step transitions are displayed using this accumulator.
074     */
075    private IaCStreamingAccumulator accumulator = null;
076
077    /**
078     * Actor reference for the distributed log store.
079     * Used to send log messages asynchronously to avoid blocking workflow execution.
080     */
081    private ActorRef<DistributedLogStore> logStoreActor;
082
083    /**
084     * Direct reference to the log store for synchronous read operations.
085     * Reads don't need to go through the actor since they don't need serialization.
086     */
087    private DistributedLogStore logStore;
088
089    /**
090     * Dedicated executor service for DB writes.
091     * Should be a single-threaded pool to ensure serialized writes.
092     */
093    private ExecutorService dbExecutor;
094
095    /**
096     * Session ID for the current workflow execution.
097     */
098    private long sessionId = -1;
099
100    /**
101     * Constructs a NodeGroupInterpreter that wraps the specified NodeGroup.
102     *
103     * @param nodeGroup the {@link NodeGroup} instance to wrap
104     * @param system the actor system for workflow execution
105     */
106    public NodeGroupInterpreter(NodeGroup nodeGroup, IIActorSystem system) {
107        super();
108        this.nodeGroup = nodeGroup;
109        this.system = system;
110    }
111
112    /**
113     * Creates Node objects for all hosts in the specified group.
114     *
115     * <p>Delegates to the wrapped {@link NodeGroup#createNodesForGroup(String)} method.</p>
116     *
117     * @param groupName the name of the group from the inventory file
118     * @return the list of created Node objects
119     */
120    public List<Node> createNodesForGroup(String groupName) {
121        return nodeGroup.createNodesForGroup(groupName);
122    }
123
124    /**
125     * Creates a single Node for localhost execution.
126     *
127     * <p>Delegates to the wrapped {@link NodeGroup#createLocalNode()} method.</p>
128     *
129     * @return a list containing a single localhost Node
130     */
131    public List<Node> createLocalNode() {
132        return nodeGroup.createLocalNode();
133    }
134
135    /**
136     * Gets the inventory object.
137     *
138     * @return the loaded inventory, or null if not loaded
139     */
140    public InventoryParser.Inventory getInventory() {
141        return nodeGroup.getInventory();
142    }
143
144    /**
145     * Gets the wrapped NodeGroup instance.
146     *
147     * <p>This allows direct access to the underlying POJO when needed.</p>
148     *
149     * @return the wrapped NodeGroup
150     */
151    public NodeGroup getNodeGroup() {
152        return nodeGroup;
153    }
154
155    /**
156     * Sets the overlay directory for YAML overlay feature.
157     *
158     * <p>When an overlay directory is set, workflows will be loaded with
159     * overlay applied, allowing environment-specific configuration.</p>
160     *
161     * @param overlayDir the path to the overlay directory containing overlay-conf.yaml
162     */
163    public void setOverlayDir(String overlayDir) {
164        this.overlayDir = overlayDir;
165    }
166
167    /**
168     * Gets the overlay directory path.
169     *
170     * @return the overlay directory path, or null if not set
171     */
172    public String getOverlayDir() {
173        return overlayDir;
174    }
175
176    /**
177     * Sets verbose mode for detailed output.
178     *
179     * <p>When enabled, displays full YAML for each transition in cowsay output
180     * instead of the truncated version.</p>
181     *
182     * @param verbose true to enable verbose output
183     */
184    public void setVerbose(boolean verbose) {
185        this.verbose = verbose;
186    }
187
188    /**
189     * Checks if verbose mode is enabled.
190     *
191     * @return true if verbose mode is enabled
192     */
193    public boolean isVerbose() {
194        return verbose;
195    }
196
197    /**
198     * Sets the IaCStreamingAccumulator for displaying workflow steps.
199     *
200     * <p>When set, workflow step transitions are displayed using cowsay ASCII art
201     * via this accumulator. The accumulator's cowfile setting determines which
202     * character is used.</p>
203     *
204     * @param accumulator the accumulator to use for cowsay display
205     */
206    public void setAccumulator(IaCStreamingAccumulator accumulator) {
207        this.accumulator = accumulator;
208    }
209
210    /**
211     * Gets the IaCStreamingAccumulator for displaying workflow steps.
212     *
213     * @return the cowsay accumulator, or null if not set
214     */
215    public IaCStreamingAccumulator getAccumulator() {
216        return accumulator;
217    }
218
219    /**
220     * Sets the distributed log store for structured logging.
221     *
222     * <p>Database writes are performed asynchronously via the logStore actor
223     * using the dedicated dbExecutor to avoid blocking workflow execution.
224     * Direct reads can use the logStore reference directly.</p>
225     *
226     * @param logStore the log store instance (for direct reads)
227     * @param logStoreActor the actor reference for the log store (for async writes)
228     * @param dbExecutor the dedicated executor service for DB writes
229     * @param sessionId the session ID for this execution
230     */
231    public void setLogStore(DistributedLogStore logStore,
232                            ActorRef<DistributedLogStore> logStoreActor,
233                            ExecutorService dbExecutor, long sessionId) {
234        this.logStore = logStore;
235        this.logStoreActor = logStoreActor;
236        this.dbExecutor = dbExecutor;
237        this.sessionId = sessionId;
238    }
239
240    /**
241     * Gets the log store for direct read operations.
242     *
243     * @return the log store, or null if not set
244     */
245    public DistributedLogStore getLogStore() {
246        return logStore;
247    }
248
249    /**
250     * Gets the log store actor for async write operations.
251     *
252     * @return the log store actor, or null if not set
253     */
254    public ActorRef<DistributedLogStore> getLogStoreActor() {
255        return logStoreActor;
256    }
257
258    /**
259     * Gets the DB executor service.
260     *
261     * @return the DB executor service, or null if not set
262     */
263    public ExecutorService getDbExecutor() {
264        return dbExecutor;
265    }
266
267    /**
268     * Gets the session ID.
269     *
270     * @return the session ID, or -1 if not set
271     */
272    public long getSessionId() {
273        return sessionId;
274    }
275
276    /**
277     * Hook called when entering a transition during workflow execution.
278     *
279     * <p>Displays the workflow name and transition definition using cowsay.
280     * In normal mode, shows first 10 lines. In verbose mode, shows the full YAML
281     * after the cowsay output.</p>
282     *
283     * @param transition the transition being entered
284     */
285    @Override
286    protected void onEnterTransition(Transition transition) {
287        // Get workflow name
288        String workflowName = (getCode() != null && getCode().getName() != null)
289                ? getCode().getName()
290                : "unknown-workflow";
291
292        // Get YAML-formatted output (first 10 lines for cowsay)
293        String yamlText = transition.toYamlString(10).trim();
294
295        // Render cowsay output
296        String cowsayOutput;
297        if (accumulator != null) {
298            cowsayOutput = accumulator.renderCowsay(workflowName, yamlText);
299        } else {
300            // Fallback to simple text if no accumulator
301            cowsayOutput = "[" + workflowName + "]\n" + yamlText;
302        }
303
304        // Send cowsay output to outputMultiplexer (loose coupling via ActorSystem)
305        IIActorRef<?> multiplexer = system.getIIActor("outputMultiplexer");
306        // Use actor name as source (consistent across all output)
307        String actorName = selfActorRef != null ? selfActorRef.getName() : "unknown";
308        if (multiplexer != null) {
309            JSONObject arg = new JSONObject();
310            arg.put("source", actorName);
311            arg.put("type", "cowsay");
312            arg.put("data", cowsayOutput);
313            multiplexer.callByActionName("add", arg.toString());
314        }
315
316        // In verbose mode, also send the full YAML
317        if (verbose) {
318            String fullYaml = transition.toYamlString(-1);
319            String verboseOutput = "--- Full transition YAML ---\n" + fullYaml + "\n----------------------------";
320            if (multiplexer != null) {
321                JSONObject arg = new JSONObject();
322                arg.put("source", actorName);
323                arg.put("type", "verbose");
324                arg.put("data", verboseOutput);
325                multiplexer.callByActionName("add", arg.toString());
326            }
327        }
328
329        // Log to distributed log store asynchronously
330        if (logStoreActor != null && sessionId >= 0) {
331            String label = transition.getLabel();
332            if (label == null && transition.getStates() != null && transition.getStates().size() >= 2) {
333                label = transition.getStates().get(0) + " -> " + transition.getStates().get(1);
334            }
335            final String finalLabel = label;
336            final String message = "Entering transition: " + yamlText.split("\n")[0];
337            // Fire-and-forget: don't wait for DB write to complete
338            logStoreActor.tell(
339                store -> store.log(sessionId, "nodeGroup", finalLabel, LogLevel.INFO, message),
340                dbExecutor);
341        }
342    }
343
344    /**
345     * Hook called after a transition completes (success or failure).
346     *
347     * <p>Logs the transition result to the distributed log store for
348     * workflow execution reporting.</p>
349     *
350     * @param transition the transition that was attempted
351     * @param success true if the transition succeeded, false if it failed
352     * @param result the ActionResult from executing the transition's actions
353     */
354    @Override
355    protected void onExitTransition(Transition transition, boolean success, ActionResult result) {
356        if (logStoreActor == null || sessionId < 0) {
357            return;
358        }
359
360        String label = transition.getLabel();
361        if (label == null && transition.getStates() != null && transition.getStates().size() >= 2) {
362            label = transition.getStates().get(0) + " -> " + transition.getStates().get(1);
363        }
364        final String finalLabel = label;
365
366        // noteを取得(60文字または最初の行まで)
367        String note = getTransitionNote(transition);
368
369        String status = success ? "SUCCESS" : "FAILED";
370        String resultMsg = result != null ? result.getResult() : "";
371        // Truncate long result messages
372        if (resultMsg.length() > 500) {
373            resultMsg = resultMsg.substring(0, 500) + "...";
374        }
375        // noteがあればメッセージに含める
376        final String message = "Transition " + status + ": " + finalLabel +
377                (note.isEmpty() ? "" : " [" + note + "]") +
378                (resultMsg.isEmpty() ? "" : " - " + resultMsg);
379
380        LogLevel level = success ? LogLevel.INFO : LogLevel.WARN;
381
382        // Fire-and-forget: don't wait for DB write to complete
383        logStoreActor.tell(
384            store -> store.log(sessionId, "nodeGroup", finalLabel, level, message),
385            dbExecutor);
386    }
387
388    /**
389     * transitionのnoteを取得する。60文字または最初の行までに制限。
390     * noteがない場合は空文字列を返す。
391     */
392    private String getTransitionNote(Transition transition) {
393        String note = transition.getNote();
394        if (note == null || note.isEmpty()) {
395            return "";
396        }
397        // 最初の行のみ
398        int newlineIdx = note.indexOf('\n');
399        if (newlineIdx > 0) {
400            note = note.substring(0, newlineIdx);
401        }
402        // 60文字まで
403        if (note.length() > 60) {
404            note = note.substring(0, 57) + "...";
405        }
406        return note.trim();
407    }
408}