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.report;
019
020import com.scivicslab.pojoactor.core.ActionResult;
021import com.scivicslab.pojoactor.core.CallableByActionName;
022import com.scivicslab.pojoactor.core.JsonState;
023import com.scivicslab.pojoactor.workflow.ActorSystemAware;
024import com.scivicslab.pojoactor.workflow.IIActorRef;
025import com.scivicslab.pojoactor.workflow.IIActorSystem;
026import com.scivicslab.pojoactor.workflow.IIActorRefAware;
027
028import org.json.JSONObject;
029import org.yaml.snakeyaml.Yaml;
030
031import java.io.InputStream;
032import java.nio.file.Files;
033import java.nio.file.Path;
034import java.nio.file.Paths;
035import java.util.ArrayList;
036import java.util.List;
037import java.util.Map;
038import java.util.Set;
039import java.util.logging.Logger;
040
041/**
042 * Section-based workflow report builder.
043 *
044 * <p>Assembles report sections from two sources:</p>
045 * <ol>
046 *   <li><strong>Legacy sections</strong> - Added via {@code addWorkflowInfo} and {@code addJsonStateSection}</li>
047 *   <li><strong>Child actor sections</strong> - {@link SectionBuilder} actors created as children</li>
048 * </ol>
049 *
050 * <h2>Usage with child actor sections (recommended):</h2>
051 * <pre>{@code
052 * steps:
053 *   - states: ["0", "1"]
054 *     actions:
055 *       - actor: loader
056 *         method: createChild
057 *         arguments: ["ROOT", "reportBuilder", "...ReportBuilder"]
058 *       - actor: loader
059 *         method: createChild
060 *         arguments: ["reportBuilder", "wfName", "...WorkflowNameSection"]
061 *       - actor: loader
062 *         method: createChild
063 *         arguments: ["reportBuilder", "wfDesc", "...WorkflowDescriptionSection"]
064 *
065 *   - states: ["1", "end"]
066 *     actions:
067 *       - actor: reportBuilder
068 *         method: report
069 * }</pre>
070 *
071 * <h2>Legacy usage:</h2>
072 * <pre>{@code
073 * steps:
074 *   - states: ["0", "1"]
075 *     actions:
076 *       - actor: loader
077 *         method: createChild
078 *         arguments: ["ROOT", "reportBuilder", "...ReportBuilder"]
079 *       - actor: reportBuilder
080 *         method: addWorkflowInfo
081 *
082 *   - states: ["1", "end"]
083 *     actions:
084 *       - actor: reportBuilder
085 *         method: report
086 * }</pre>
087 *
088 * <h2>Actions:</h2>
089 * <ul>
090 *   <li>{@code addWorkflowInfo} - Add workflow metadata section (legacy)</li>
091 *   <li>{@code addJsonStateSection} - Add actor's JsonState as YAML (legacy)</li>
092 *   <li>{@code report} - Build and output the report to outputMultiplexer</li>
093 * </ul>
094 *
095 * @author devteam@scivicslab.com
096 * @since 2.15.0
097 */
098public class ReportBuilder implements CallableByActionName, ActorSystemAware, IIActorRefAware {
099
100    private static final String CLASS_NAME = ReportBuilder.class.getName();
101    private static final Logger logger = Logger.getLogger(CLASS_NAME);
102
103    private final List<ReportSection> sections = new ArrayList<>();
104    private IIActorSystem system;
105    private IIActorRef<?> selfRef;
106
107    /**
108     * Default constructor for use with loader.createChild.
109     */
110    public ReportBuilder() {
111    }
112
113    @Override
114    public void setActorSystem(IIActorSystem system) {
115        this.system = system;
116        logger.info("ReportBuilder: ActorSystem set");
117    }
118
119    @Override
120    public void setIIActorRef(IIActorRef<?> actorRef) {
121        this.selfRef = actorRef;
122        logger.info("ReportBuilder: IIActorRef set");
123    }
124
125    // ========================================================================
126    // CallableByActionName implementation
127    // ========================================================================
128
129    @Override
130    public ActionResult callByActionName(String actionName, String args) {
131        logger.info("ReportBuilder.callByActionName: " + actionName);
132
133        return switch (actionName) {
134            case "addWorkflowInfo" -> addWorkflowInfo(args);
135            case "addJsonStateSection" -> addJsonStateSection(args);
136            case "report" -> report(args);
137            default -> new ActionResult(false, "Unknown action: " + actionName);
138        };
139    }
140
141    // ========================================================================
142    // Actions
143    // ========================================================================
144
145    /**
146     * Adds workflow info section.
147     */
148    private ActionResult addWorkflowInfo(String args) {
149        String workflowPath = getWorkflowPathFromNodeGroup();
150        if (workflowPath == null) {
151            return new ActionResult(false, "Could not get workflow path from nodeGroup");
152        }
153
154        String name = null;
155        String description = null;
156
157        // Try to read workflow YAML for name and description
158        try {
159            Path path = Paths.get(workflowPath);
160            if (!Files.exists(path)) {
161                path = Paths.get(System.getProperty("user.dir"), workflowPath);
162            }
163
164            if (Files.exists(path)) {
165                try (InputStream is = Files.newInputStream(path)) {
166                    Yaml yaml = new Yaml();
167                    Map<String, Object> data = yaml.load(is);
168                    if (data != null) {
169                        name = (String) data.get("name");
170                        Object descObj = data.get("description");
171                        description = descObj != null ? descObj.toString().trim() : null;
172                    }
173                }
174            }
175        } catch (Exception e) {
176            logger.warning("ReportBuilder.addWorkflowInfo: Could not read workflow file: " + e.getMessage());
177        }
178
179        addSection(new WorkflowInfoSection(workflowPath, name, description));
180        return new ActionResult(true, "Workflow info section added");
181    }
182
183    /**
184     * Adds JsonState section for specified actor.
185     */
186    private ActionResult addJsonStateSection(String args) {
187        String actorName;
188        String path = "";
189
190        try {
191            JSONObject json = new JSONObject(args);
192            actorName = json.getString("actor");
193            path = json.optString("path", "");
194        } catch (Exception e) {
195            return new ActionResult(false, "Invalid arguments: " + e.getMessage() +
196                ". Expected: {\"actor\": \"name\", \"path\": \"optional\"}");
197        }
198
199        if (system == null) {
200            return new ActionResult(false, "ActorSystem not available");
201        }
202
203        IIActorRef<?> targetActor = system.getIIActor(actorName);
204        if (targetActor == null) {
205            return new ActionResult(false, "Actor not found: " + actorName);
206        }
207
208        JsonState jsonState = targetActor.json();
209        if (jsonState == null) {
210            return new ActionResult(false, "Actor has no JsonState: " + actorName);
211        }
212
213        String yamlContent = jsonState.toStringOfYaml(path);
214        addSection(new JsonStateSection(actorName, yamlContent));
215
216        return new ActionResult(true, "JsonState section added for " + actorName);
217    }
218
219    /**
220     * Builds and outputs the report.
221     */
222    private ActionResult report(String args) {
223        String reportContent = build();
224        reportToMultiplexer(reportContent);
225        return new ActionResult(true, reportContent);
226    }
227
228    // ========================================================================
229    // Core Methods
230    // ========================================================================
231
232    /**
233     * Adds a section to the report.
234     *
235     * @param section the section to add
236     */
237    public void addSection(ReportSection section) {
238        if (section != null) {
239            sections.add(section);
240        }
241    }
242
243    /**
244     * Builds the report string.
245     *
246     * <p>Collects sections from two sources in order:</p>
247     * <ol>
248     *   <li>Legacy sections added via {@code addWorkflowInfo} and {@code addJsonStateSection}</li>
249     *   <li>Child actor sections implementing {@link SectionBuilder} (in creation order)</li>
250     * </ol>
251     *
252     * @return the formatted report string
253     */
254    public String build() {
255        StringBuilder sb = new StringBuilder();
256
257        // Header
258        sb.append("=== Workflow Execution Report ===\n");
259
260        // 1. Output legacy sections first (in order added)
261        for (ReportSection section : sections) {
262            appendSection(sb, section.getTitle(), section.getContent());
263        }
264
265        // 2. Output child actor sections (in creation order)
266        if (selfRef != null && system != null) {
267            Set<String> childNames = selfRef.getNamesOfChildren();
268            for (String childName : childNames) {
269                IIActorRef<?> childRef = system.getIIActor(childName);
270                if (childRef == null) continue;
271
272                // Call generate action on the child
273                ActionResult generateResult = childRef.callByActionName("generate", "");
274                if (!generateResult.isSuccess()) {
275                    // Child doesn't support generate action, skip it
276                    continue;
277                }
278
279                String content = generateResult.getResult();
280                if (content == null || content.isEmpty()) {
281                    continue;
282                }
283
284                // Get title (may be empty)
285                String title = null;
286                ActionResult titleResult = childRef.callByActionName("getTitle", "");
287                if (titleResult.isSuccess()) {
288                    String t = titleResult.getResult();
289                    if (t != null && !t.isEmpty()) {
290                        title = t;
291                    }
292                }
293
294                appendSection(sb, title, content);
295            }
296        }
297
298        return sb.toString();
299    }
300
301    private void appendSection(StringBuilder sb, String title, String content) {
302        if (content != null && !content.isEmpty()) {
303            if (title != null && !title.isEmpty()) {
304                sb.append("\n--- ").append(title).append(" ---\n");
305            }
306            sb.append(content).append("\n");
307        }
308    }
309
310    // ========================================================================
311    // Helper Methods
312    // ========================================================================
313
314    private String getWorkflowPathFromNodeGroup() {
315        if (system == null) return null;
316
317        IIActorRef<?> nodeGroup = system.getIIActor("nodeGroup");
318        if (nodeGroup == null) return null;
319
320        ActionResult result = nodeGroup.callByActionName("getWorkflowPath", "");
321        return result.isSuccess() ? result.getResult() : null;
322    }
323
324    private void reportToMultiplexer(String data) {
325        if (system == null) return;
326
327        IIActorRef<?> multiplexer = system.getIIActor("outputMultiplexer");
328        if (multiplexer == null) return;
329
330        JSONObject arg = new JSONObject();
331        arg.put("source", "report-builder");
332        arg.put("type", "plugin-result");
333        arg.put("data", data);
334        multiplexer.callByActionName("add", arg.toString());
335    }
336}