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.IOException;
022import java.io.InputStream;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.util.Arrays;
026import java.util.Comparator;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.concurrent.Callable;
031import java.util.logging.LogManager;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034
035import picocli.CommandLine;
036import picocli.CommandLine.Command;
037import picocli.CommandLine.Option;
038
039/**
040 * Main command-line interface for actor-IaC.
041 *
042 * <p>This is the entry point for all actor-IaC commands. Use subcommands to
043 * execute specific operations:</p>
044 *
045 * <h2>Subcommands</h2>
046 * <ul>
047 *   <li>{@code run} - Execute workflows</li>
048 *   <li>{@code list} - List available workflows</li>
049 *   <li>{@code describe} - Describe workflow structure</li>
050 *   <li>{@code logs} - Query execution logs</li>
051 *   <li>{@code log-server} - Start H2 log server</li>
052 * </ul>
053 *
054 * <h2>Usage Examples</h2>
055 * <pre>
056 * actor-iac run -d ./workflows -w deploy
057 * actor-iac list -d ./workflows
058 * actor-iac logs --db ./logs --list
059 * actor-iac log-server --db ./logs
060 * </pre>
061 *
062 * @author devteam@scivics-lab.com
063 * @since 2.7.0
064 */
065@Command(
066    name = "actor-iac",
067    mixinStandardHelpOptions = true,
068    version = "actor-IaC 2.10.0",
069    description = "Infrastructure as Code workflow automation tool.",
070    subcommands = {
071        RunCLI.class,
072        ListWorkflowsCLI.class,
073        DescribeCLI.class,
074        LogsCLI.class,
075        LogServerCLI.class,
076        MergeLogsCLI.class
077    }
078)
079public class WorkflowCLI implements Callable<Integer> {
080
081    /**
082     * Main entry point.
083     *
084     * @param args command line arguments
085     */
086    public static void main(String[] args) {
087        // Load logging configuration from resources
088        try (InputStream is = WorkflowCLI.class.getResourceAsStream("/logging.properties")) {
089            if (is != null) {
090                LogManager.getLogManager().readConfiguration(is);
091            }
092        } catch (IOException e) {
093            System.err.println("Warning: Failed to load logging.properties: " + e.getMessage());
094        }
095
096        int exitCode = new CommandLine(new WorkflowCLI()).execute(args);
097        System.exit(exitCode);
098    }
099
100    /**
101     * Called when no subcommand is specified.
102     * Shows help message.
103     */
104    @Override
105    public Integer call() {
106        System.out.println("actor-IaC - Infrastructure as Code workflow automation tool");
107        System.out.println();
108        System.out.println("Usage: actor-iac <command> [options]");
109        System.out.println();
110        System.out.println("Commands:");
111        System.out.println("  run          Execute a workflow");
112        System.out.println("  list         List available workflows");
113        System.out.println("  describe     Describe workflow structure");
114        System.out.println("  log-search   Search execution logs");
115        System.out.println("  log-serve    Start H2 log server for centralized logging");
116        System.out.println("  log-merge    Merge scattered log databases into one");
117        System.out.println();
118        System.out.println("Examples:");
119        System.out.println("  actor-iac run -d ./workflows -w deploy");
120        System.out.println("  actor-iac run -d ./workflows -w deploy -i inventory.ini -g webservers");
121        System.out.println("  actor-iac list -d ./workflows");
122        System.out.println("  actor-iac log-search --db ./logs --list");
123        System.out.println("  actor-iac log-serve --db ./logs/shared");
124        System.out.println("  actor-iac log-merge --scan ./workflows --target ./logs/merged");
125        System.out.println();
126        System.out.println("Use 'actor-iac <command> --help' for more information about a command.");
127        return 0;
128    }
129}
130
131/**
132 * Subcommand to list workflows discovered under a directory.
133 */
134@Command(
135    name = "list",
136    mixinStandardHelpOptions = true,
137    version = "actor-IaC list 2.10.0",
138    description = "List workflows discovered under --dir."
139)
140class ListWorkflowsCLI implements Callable<Integer> {
141
142    @Option(
143        names = {"-d", "--dir"},
144        required = true,
145        description = "Directory containing workflow files (searched recursively)"
146    )
147    private File workflowDir;
148
149    @Override
150    public Integer call() {
151        if (!workflowDir.isDirectory()) {
152            System.err.println("Not a directory: " + workflowDir);
153            return 1;
154        }
155
156        List<WorkflowDisplay> displays = scanWorkflowsForDisplay(workflowDir);
157        if (displays.isEmpty()) {
158            System.out.println("No workflow files found under " + workflowDir.getAbsolutePath());
159            return 0;
160        }
161
162        System.out.println("Available workflows (directory: " + workflowDir.getAbsolutePath() + ")");
163        System.out.println("-".repeat(90));
164        System.out.printf("%-4s %-25s %-35s %s%n", "#", "File (-w)", "Path", "Workflow Name (in logs)");
165        System.out.println("-".repeat(90));
166        int index = 1;
167        for (WorkflowDisplay display : displays) {
168            String workflowName = display.workflowName() != null ? display.workflowName() : "(no name)";
169            System.out.printf("%2d.  %-25s %-35s %s%n",
170                index++, display.baseName(), display.relativePath(), workflowName);
171        }
172        System.out.println("-".repeat(90));
173        System.out.println("Use 'actor-iac run -d " + workflowDir + " -w <File>' to execute a workflow.");
174        return 0;
175    }
176
177    private static List<WorkflowDisplay> scanWorkflowsForDisplay(File directory) {
178        if (directory == null) {
179            return List.of();
180        }
181
182        try (Stream<Path> paths = Files.walk(directory.toPath())) {
183            Map<String, File> uniqueFiles = new LinkedHashMap<>();
184            paths.filter(Files::isRegularFile)
185                 .filter(path -> {
186                     String name = path.getFileName().toString().toLowerCase();
187                     return name.endsWith(".yaml") || name.endsWith(".yml")
188                         || name.endsWith(".json") || name.endsWith(".xml");
189                 })
190                 .forEach(path -> uniqueFiles.putIfAbsent(path.toFile().getAbsolutePath(), path.toFile()));
191
192            Path basePath = directory.toPath();
193            return uniqueFiles.values().stream()
194                .map(file -> new WorkflowDisplay(
195                    getBaseName(file.getName()),
196                    relativize(basePath, file.toPath()),
197                    extractWorkflowName(file)))
198                .sorted(Comparator.comparing(WorkflowDisplay::baseName, String.CASE_INSENSITIVE_ORDER))
199                .collect(Collectors.toList());
200        } catch (IOException e) {
201            System.err.println("Failed to scan workflows: " + e.getMessage());
202            return List.of();
203        }
204    }
205
206    private static String getBaseName(String fileName) {
207        int dotIndex = fileName.lastIndexOf('.');
208        if (dotIndex > 0) {
209            return fileName.substring(0, dotIndex);
210        }
211        return fileName;
212    }
213
214    private static String relativize(Path base, Path target) {
215        try {
216            return base.relativize(target).toString();
217        } catch (IllegalArgumentException e) {
218            return target.toAbsolutePath().toString();
219        }
220    }
221
222    private static String extractWorkflowName(File file) {
223        String fileName = file.getName().toLowerCase();
224        if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) {
225            return extractNameFromYaml(file);
226        } else if (fileName.endsWith(".json")) {
227            return extractNameFromJson(file);
228        }
229        return null;
230    }
231
232    private static String extractNameFromYaml(File file) {
233        try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(file))) {
234            String line;
235            while ((line = reader.readLine()) != null) {
236                line = line.trim();
237                if (line.startsWith("name:")) {
238                    String value = line.substring(5).trim();
239                    if ((value.startsWith("\"") && value.endsWith("\"")) ||
240                        (value.startsWith("'") && value.endsWith("'"))) {
241                        value = value.substring(1, value.length() - 1);
242                    }
243                    return value.isEmpty() ? null : value;
244                }
245                if (line.startsWith("steps:") || line.startsWith("vertices:")) {
246                    break;
247                }
248            }
249        } catch (IOException e) {
250            // Ignore
251        }
252        return null;
253    }
254
255    private static String extractNameFromJson(File file) {
256        try (java.io.FileReader reader = new java.io.FileReader(file)) {
257            org.json.JSONTokener tokener = new org.json.JSONTokener(reader);
258            org.json.JSONObject json = new org.json.JSONObject(tokener);
259            return json.optString("name", null);
260        } catch (Exception e) {
261            // Ignore
262        }
263        return null;
264    }
265
266    private static record WorkflowDisplay(String baseName, String relativePath, String workflowName) {}
267}