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.cli;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.InputStream;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.Callable;
029import java.util.stream.Stream;
030
031import com.scivicslab.pojoactor.workflow.kustomize.WorkflowKustomizer;
032
033import picocli.CommandLine.Command;
034import picocli.CommandLine.Option;
035
036/**
037 * CLI subcommand for displaying workflow descriptions.
038 *
039 * <p>Usage examples:</p>
040 * <pre>
041 * # Show workflow description only
042 * actor-iac describe -d ./workflows -w my-workflow
043 *
044 * # Show workflow description with step descriptions
045 * actor-iac describe -d ./workflows -w my-workflow --steps
046 *
047 * # With overlay
048 * actor-iac describe -d ./workflows -w my-workflow -o ./overlays/env --steps
049 * </pre>
050 *
051 * @author devteam@scivics-lab.com
052 * @since 2.10.0
053 */
054@Command(
055    name = "describe",
056    mixinStandardHelpOptions = true,
057    version = "actor-IaC describe 2.10.0",
058    description = "Display workflow and step descriptions."
059)
060public class DescribeCLI implements Callable<Integer> {
061
062    @Option(
063        names = {"-d", "--dir"},
064        description = "Directory containing workflow files (searched recursively)",
065        required = true
066    )
067    private File workflowDir;
068
069    @Option(
070        names = {"-w", "--workflow"},
071        description = "Name of the workflow to describe (with or without extension)",
072        required = true
073    )
074    private String workflowName;
075
076    @Option(
077        names = {"-o", "--overlay"},
078        description = "Overlay directory containing overlay-conf.yaml"
079    )
080    private File overlayDir;
081
082    @Option(
083        names = {"--steps"},
084        description = "Also display descriptions of each step"
085    )
086    private boolean showSteps;
087
088    /** Cache of discovered workflow files: name -> File */
089    private final Map<String, File> workflowCache = new HashMap<>();
090
091    @Override
092    public Integer call() {
093        // Scan workflow directory
094        scanWorkflowDirectory(workflowDir);
095
096        // Find the workflow file
097        File workflowFile = findWorkflowFile(workflowName);
098        if (workflowFile == null) {
099            System.err.println("Workflow not found: " + workflowName);
100            System.err.println("Available workflows: " + workflowCache.keySet());
101            return 1;
102        }
103
104        // Load YAML (with overlay if specified)
105        Map<String, Object> yaml;
106        if (overlayDir != null) {
107            yaml = loadYamlWithOverlay(workflowFile);
108        } else {
109            yaml = loadYamlFile(workflowFile);
110        }
111
112        if (yaml == null) {
113            System.err.println("Failed to load workflow: " + workflowFile);
114            return 1;
115        }
116
117        // Print workflow description
118        printWorkflowDescription(workflowFile, yaml);
119
120        return 0;
121    }
122
123    /**
124     * Prints workflow description.
125     */
126    @SuppressWarnings("unchecked")
127    private void printWorkflowDescription(File file, Map<String, Object> yaml) {
128        String name = (String) yaml.getOrDefault("name", "(unnamed)");
129        String description = (String) yaml.get("description");
130
131        System.out.println("Workflow: " + name);
132        System.out.println("File: " + file.getAbsolutePath());
133        if (overlayDir != null) {
134            System.out.println("Overlay: " + overlayDir.getAbsolutePath());
135        }
136        System.out.println();
137
138        // Workflow-level description
139        System.out.println("Description:");
140        if (description != null && !description.isBlank()) {
141            for (String line : description.split("\n")) {
142                System.out.println("  " + line);
143            }
144        } else {
145            System.out.println("  (no description)");
146        }
147
148        // Step descriptions (if --steps flag is set)
149        if (showSteps) {
150            System.out.println();
151            System.out.println("Steps:");
152
153            List<Map<String, Object>> steps = (List<Map<String, Object>>) yaml.get("steps");
154            if (steps == null || steps.isEmpty()) {
155                System.out.println("  (no steps defined)");
156                return;
157            }
158
159            for (Map<String, Object> step : steps) {
160                List<String> states = (List<String>) step.get("states");
161                String vertexName = (String) step.get("vertexName");
162                String stepDescription = (String) step.get("description");
163
164                String stateTransition = (states != null && states.size() >= 2)
165                    ? states.get(0) + " -> " + states.get(1)
166                    : "?";
167
168                String displayName = (vertexName != null) ? vertexName : "(unnamed)";
169
170                System.out.println();
171                System.out.println("  [" + stateTransition + "] " + displayName);
172                if (stepDescription != null && !stepDescription.isBlank()) {
173                    for (String line : stepDescription.split("\n")) {
174                        System.out.println("    " + line);
175                    }
176                } else {
177                    System.out.println("    (no description)");
178                }
179            }
180        }
181    }
182
183    /**
184     * Scans the workflow directory recursively for workflow files.
185     */
186    private void scanWorkflowDirectory(File directory) {
187        if (directory == null || !directory.exists()) {
188            return;
189        }
190
191        try (Stream<Path> paths = Files.walk(directory.toPath())) {
192            paths.filter(Files::isRegularFile)
193                 .filter(path -> {
194                     String name = path.getFileName().toString().toLowerCase();
195                     return name.endsWith(".yaml") || name.endsWith(".yml")
196                         || name.endsWith(".json") || name.endsWith(".xml");
197                 })
198                 .forEach(path -> {
199                     File file = path.toFile();
200                     String baseName = getBaseName(file.getName());
201                     workflowCache.putIfAbsent(baseName, file);
202                     workflowCache.putIfAbsent(file.getName(), file);
203                 });
204        } catch (Exception e) {
205            System.err.println("Warning: Failed to scan directory: " + e.getMessage());
206        }
207    }
208
209    /**
210     * Finds a workflow file by name.
211     */
212    private File findWorkflowFile(String name) {
213        // Try exact match first
214        File file = workflowCache.get(name);
215        if (file != null) {
216            return file;
217        }
218
219        // Try with extensions
220        for (String ext : new String[]{".yaml", ".yml", ".json", ".xml"}) {
221            file = workflowCache.get(name + ext);
222            if (file != null) {
223                return file;
224            }
225        }
226
227        return null;
228    }
229
230    /**
231     * Loads a YAML file.
232     */
233    private Map<String, Object> loadYamlFile(File file) {
234        try (InputStream is = new FileInputStream(file)) {
235            org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml();
236            return yaml.load(is);
237        } catch (Exception e) {
238            System.err.println("Failed to load YAML file: " + file + " - " + e.getMessage());
239            return null;
240        }
241    }
242
243    /**
244     * Loads a YAML file with overlay applied.
245     */
246    private Map<String, Object> loadYamlWithOverlay(File workflowFile) {
247        try {
248            WorkflowKustomizer kustomizer = new WorkflowKustomizer();
249            Map<String, Map<String, Object>> workflows = kustomizer.build(overlayDir.toPath());
250
251            String targetName = workflowFile.getName();
252            for (Map.Entry<String, Map<String, Object>> entry : workflows.entrySet()) {
253                if (entry.getKey().equals(targetName)) {
254                    return entry.getValue();
255                }
256            }
257
258            // Fall back to raw file
259            return loadYamlFile(workflowFile);
260        } catch (Exception e) {
261            System.err.println("Failed to apply overlay, loading raw file: " + e.getMessage());
262            return loadYamlFile(workflowFile);
263        }
264    }
265
266    /**
267     * Gets the base name of a file (without extension).
268     */
269    private static String getBaseName(String fileName) {
270        int dotIndex = fileName.lastIndexOf('.');
271        if (dotIndex > 0) {
272            return fileName.substring(0, dotIndex);
273        }
274        return fileName;
275    }
276}