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.cli;
019
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.util.Comparator;
026import java.util.List;
027import java.util.concurrent.Callable;
028import java.util.logging.LogManager;
029import java.util.stream.Collectors;
030import java.util.stream.Stream;
031
032import picocli.CommandLine;
033import picocli.CommandLine.Command;
034import picocli.CommandLine.Option;
035
036/**
037 * Main command-line interface for actor-IaC.
038 *
039 * <p>This is the entry point for all actor-IaC commands. Use subcommands to
040 * execute specific operations:</p>
041 *
042 * <h2>Subcommands</h2>
043 * <ul>
044 *   <li>{@code run} - Execute workflows</li>
045 *   <li>{@code list} - List available workflows</li>
046 *   <li>{@code describe} - Describe workflow structure</li>
047 *   <li>{@code logs} - Query execution logs</li>
048 *   <li>{@code db-clear} - Clear (delete) the log database</li>
049 * </ul>
050 *
051 * <h2>Usage Examples</h2>
052 * <pre>
053 * actor-iac run -d ./workflows -w deploy
054 * actor-iac list -d ./workflows
055 * actor-iac logs --db ./logs --list
056 * </pre>
057 *
058 * @author devteam@scivicslab.com
059 * @since 2.7.0
060 */
061@Command(
062    name = "actor-iac",
063    mixinStandardHelpOptions = true,
064    versionProvider = VersionProvider.class,
065    description = "Infrastructure as Code workflow automation tool.",
066    subcommands = {
067        RunCLI.class,
068        ListWorkflowsCLI.class,
069        DescribeCLI.class,
070        LogsCLI.class,
071        MergeLogsCLI.class,
072        DbClearCLI.class
073    }
074)
075public class WorkflowCLI implements Callable<Integer> {
076
077    /**
078     * Main entry point.
079     *
080     * @param args command line arguments
081     */
082    public static void main(String[] args) {
083        // Load logging configuration from resources
084        try (InputStream is = WorkflowCLI.class.getResourceAsStream("/logging.properties")) {
085            if (is != null) {
086                LogManager.getLogManager().readConfiguration(is);
087            }
088        } catch (IOException e) {
089            System.err.println("Warning: Failed to load logging.properties: " + e.getMessage());
090        }
091
092        int exitCode = new CommandLine(new WorkflowCLI()).execute(args);
093        System.exit(exitCode);
094    }
095
096    /**
097     * Called when no subcommand is specified.
098     * Shows help message.
099     */
100    @Override
101    public Integer call() {
102        System.out.println("actor-IaC - Infrastructure as Code workflow automation tool");
103        System.out.println();
104        System.out.println("Usage: actor-iac <command> [options]");
105        System.out.println();
106        System.out.println("Commands:");
107        System.out.println("  run          Execute a workflow");
108        System.out.println("  list         List available workflows");
109        System.out.println("  describe     Describe workflow structure");
110        System.out.println("  log-search   Search execution logs");
111        System.out.println("  log-merge    Merge scattered log databases into one");
112        System.out.println();
113        System.out.println("Examples:");
114        System.out.println("  actor-iac run -w sysinfo/main-collect-sysinfo.yaml -i inventory.ini");
115        System.out.println("  actor-iac run -d ./sysinfo -w main-collect-sysinfo.yaml -i inventory.ini");
116        System.out.println("  actor-iac list -w sysinfo");
117        System.out.println("  actor-iac describe -w sysinfo/main-collect-sysinfo.yaml");
118        System.out.println("  actor-iac log-search --db ./logs --list");
119        System.out.println();
120        System.out.println("Use 'actor-iac <command> --help' for more information about a command.");
121        return 0;
122    }
123}
124
125/**
126 * Subcommand to list workflows discovered under a directory.
127 */
128@Command(
129    name = "list",
130    mixinStandardHelpOptions = true,
131    versionProvider = VersionProvider.class,
132    description = "List workflows in the specified directory."
133)
134class ListWorkflowsCLI implements Callable<Integer> {
135
136    @Option(
137        names = {"-d", "--dir"},
138        description = "Base directory. Defaults to current directory.",
139        defaultValue = "."
140    )
141    private File baseDir;
142
143    @Option(
144        names = {"-w", "--workflow"},
145        required = true,
146        description = "Workflow directory path (relative to -d)"
147    )
148    private String workflowPath;
149
150    @Option(
151        names = {"-o", "--output"},
152        description = "Output format: table, json, yaml (default: table)",
153        defaultValue = "table"
154    )
155    private String outputFormat;
156
157    @Override
158    public Integer call() {
159        File workflowDir = new File(baseDir, workflowPath);
160        if (!workflowDir.isDirectory()) {
161            System.err.println("Not a directory: " + workflowDir);
162            return 1;
163        }
164
165        List<WorkflowInfo> workflows = scanWorkflowsForDisplay(workflowDir, workflowPath);
166        if (workflows.isEmpty()) {
167            if ("json".equalsIgnoreCase(outputFormat)) {
168                System.out.println("[]");
169            } else if ("yaml".equalsIgnoreCase(outputFormat)) {
170                System.out.println("workflows: []");
171            } else {
172                System.out.println("No workflow files found in " + workflowDir.getPath());
173            }
174            return 0;
175        }
176
177        switch (outputFormat.toLowerCase()) {
178            case "json" -> printJson(workflows);
179            case "yaml" -> printYaml(workflows);
180            default -> printTable(workflows, workflowDir.getPath());
181        }
182        return 0;
183    }
184
185    private static final int WRAP_WIDTH = 70;
186    private static final String INDENT = "    ";
187
188    private void printTable(List<WorkflowInfo> workflows, String dirPath) {
189        System.out.println("Workflows in " + dirPath + ":");
190        System.out.println();
191        String separator = "-".repeat(WRAP_WIDTH + INDENT.length());
192        for (int i = 0; i < workflows.size(); i++) {
193            WorkflowInfo wf = workflows.get(i);
194            System.out.println("  " + wf.path());
195            if (wf.name() != null) {
196                System.out.println(INDENT + "name: " + wf.name());
197            }
198            if (wf.description() != null) {
199                printWrapped("description", wf.description());
200            }
201            if (i < workflows.size() - 1) {
202                System.out.println(separator);
203            }
204        }
205        System.out.println();
206    }
207
208    private void printWrapped(String label, String text) {
209        String prefix = INDENT + label + ": ";
210        String continuationIndent = INDENT + " ".repeat(label.length() + 2);
211        int maxWidth = WRAP_WIDTH;
212
213        String[] words = text.split("\\s+");
214        StringBuilder line = new StringBuilder(prefix);
215        boolean firstLine = true;
216
217        for (String word : words) {
218            int currentLen = firstLine ? line.length() : line.length();
219            if (currentLen + word.length() + 1 > maxWidth + prefix.length() && line.length() > (firstLine ? prefix.length() : continuationIndent.length())) {
220                System.out.println(line.toString());
221                line = new StringBuilder(continuationIndent);
222                firstLine = false;
223            }
224            if (line.length() > (firstLine ? prefix.length() : continuationIndent.length())) {
225                line.append(" ");
226            }
227            line.append(word);
228        }
229        if (line.length() > (firstLine ? prefix.length() : continuationIndent.length())) {
230            System.out.println(line.toString());
231        }
232    }
233
234    private void printJson(List<WorkflowInfo> workflows) {
235        org.json.JSONArray arr = new org.json.JSONArray();
236        for (WorkflowInfo wf : workflows) {
237            org.json.JSONObject obj = new org.json.JSONObject();
238            obj.put("path", wf.path());
239            if (wf.name() != null) obj.put("name", wf.name());
240            if (wf.description() != null) obj.put("description", wf.description());
241            if (wf.note() != null) obj.put("note", wf.note());
242            arr.put(obj);
243        }
244        System.out.println(arr.toString(2));
245    }
246
247    private void printYaml(List<WorkflowInfo> workflows) {
248        System.out.println("workflows:");
249        for (WorkflowInfo wf : workflows) {
250            System.out.println("  - path: " + wf.path());
251            if (wf.name() != null) {
252                System.out.println("    name: " + wf.name());
253            }
254            if (wf.description() != null) {
255                // Handle multi-line descriptions
256                if (wf.description().contains("\n")) {
257                    System.out.println("    description: |");
258                    for (String line : wf.description().split("\n")) {
259                        System.out.println("      " + line);
260                    }
261                } else {
262                    System.out.println("    description: " + wf.description());
263                }
264            }
265            if (wf.note() != null) {
266                System.out.println("    note: " + wf.note());
267            }
268        }
269    }
270
271    private static List<WorkflowInfo> scanWorkflowsForDisplay(File directory, String workflowPath) {
272        if (directory == null) {
273            return List.of();
274        }
275
276        // Non-recursive scan - only files in immediate directory
277        try (Stream<Path> paths = Files.list(directory.toPath())) {
278            return paths.filter(Files::isRegularFile)
279                 .filter(path -> {
280                     String name = path.getFileName().toString().toLowerCase();
281                     return name.endsWith(".yaml") || name.endsWith(".yml")
282                         || name.endsWith(".json") || name.endsWith(".xml");
283                 })
284                 .map(path -> {
285                     File file = path.toFile();
286                     String relPath = workflowPath.endsWith("/")
287                         ? workflowPath + file.getName()
288                         : workflowPath + "/" + file.getName();
289                     return extractWorkflowInfo(file, relPath);
290                 })
291                 .sorted(Comparator.comparing(WorkflowInfo::path, String.CASE_INSENSITIVE_ORDER))
292                 .collect(Collectors.toList());
293        } catch (IOException e) {
294            System.err.println("Failed to scan workflows: " + e.getMessage());
295            return List.of();
296        }
297    }
298
299    private static WorkflowInfo extractWorkflowInfo(File file, String path) {
300        String fileName = file.getName().toLowerCase();
301        if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) {
302            return extractWorkflowInfoFromYaml(file, path);
303        } else if (fileName.endsWith(".json")) {
304            return extractWorkflowInfoFromJson(file, path);
305        }
306        return new WorkflowInfo(path, null, null, null);
307    }
308
309    private static WorkflowInfo extractWorkflowInfoFromYaml(File file, String path) {
310        String name = null;
311        String description = null;
312        String note = null;
313
314        try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(file))) {
315            String line;
316            StringBuilder currentValue = new StringBuilder();
317            String currentField = null;
318            boolean inMultiLine = false;
319
320            while ((line = reader.readLine()) != null) {
321                // Stop at steps or vertices
322                if (line.trim().startsWith("steps:") || line.trim().startsWith("vertices:")) {
323                    break;
324                }
325
326                if (inMultiLine) {
327                    if (line.startsWith("  ") || line.startsWith("\t") || line.trim().isEmpty()) {
328                        if (!line.trim().isEmpty()) {
329                            currentValue.append(line.trim()).append(" ");
330                        }
331                        continue;
332                    } else {
333                        // End of multi-line
334                        String value = currentValue.toString().trim();
335                        if ("name".equals(currentField)) name = value;
336                        else if ("description".equals(currentField)) description = value;
337                        else if ("note".equals(currentField)) note = value;
338                        inMultiLine = false;
339                        currentValue = new StringBuilder();
340                        currentField = null;
341                    }
342                }
343
344                if (line.trim().startsWith("name:") && name == null) {
345                    String value = line.substring(line.indexOf(':') + 1).trim();
346                    if (value.equals("|") || value.equals(">")) {
347                        inMultiLine = true;
348                        currentField = "name";
349                    } else if (!value.isEmpty()) {
350                        name = stripQuotes(value);
351                    }
352                } else if (line.trim().startsWith("description:") && description == null) {
353                    String value = line.substring(line.indexOf(':') + 1).trim();
354                    if (value.equals("|") || value.equals(">")) {
355                        inMultiLine = true;
356                        currentField = "description";
357                    } else if (!value.isEmpty()) {
358                        description = stripQuotes(value);
359                    }
360                } else if (line.trim().startsWith("note:") && note == null) {
361                    String value = line.substring(line.indexOf(':') + 1).trim();
362                    if (value.equals("|") || value.equals(">")) {
363                        inMultiLine = true;
364                        currentField = "note";
365                    } else if (!value.isEmpty()) {
366                        note = stripQuotes(value);
367                    }
368                }
369            }
370
371            // Handle trailing multi-line
372            if (inMultiLine && currentValue.length() > 0) {
373                String value = currentValue.toString().trim();
374                if ("name".equals(currentField)) name = value;
375                else if ("description".equals(currentField)) description = value;
376                else if ("note".equals(currentField)) note = value;
377            }
378        } catch (IOException e) {
379            // Ignore
380        }
381        return new WorkflowInfo(path, name, description, note);
382    }
383
384    private static String stripQuotes(String value) {
385        if ((value.startsWith("\"") && value.endsWith("\"")) ||
386            (value.startsWith("'") && value.endsWith("'"))) {
387            return value.substring(1, value.length() - 1);
388        }
389        return value;
390    }
391
392    private static WorkflowInfo extractWorkflowInfoFromJson(File file, String path) {
393        try (java.io.FileReader reader = new java.io.FileReader(file)) {
394            org.json.JSONTokener tokener = new org.json.JSONTokener(reader);
395            org.json.JSONObject json = new org.json.JSONObject(tokener);
396            return new WorkflowInfo(
397                path,
398                json.optString("name", null),
399                json.optString("description", null),
400                json.optString("note", null)
401            );
402        } catch (Exception e) {
403            // Ignore
404        }
405        return new WorkflowInfo(path, null, null, null);
406    }
407
408    private static record WorkflowInfo(String path, String name, String description, String note) {}
409}