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.FileInputStream;
022import java.io.InputStream;
023import java.util.List;
024import java.util.Map;
025import java.util.concurrent.Callable;
026
027import com.scivicslab.pojoactor.workflow.kustomize.WorkflowKustomizer;
028
029import picocli.CommandLine.Command;
030import picocli.CommandLine.Option;
031
032/**
033 * CLI subcommand for displaying workflow descriptions.
034 *
035 * <p>Usage examples:</p>
036 * <pre>
037 * # Show workflow description only
038 * actor-iac describe -w sysinfo/main-collect-sysinfo.yaml
039 *
040 * # Show workflow description with step descriptions
041 * actor-iac describe -w sysinfo/main-collect-sysinfo.yaml --steps
042 *
043 * # With overlay
044 * actor-iac describe -w sysinfo/main-collect-sysinfo.yaml -o ./overlays/env --steps
045 * </pre>
046 *
047 * @author devteam@scivicslab.com
048 * @since 2.10.0
049 */
050@Command(
051    name = "describe",
052    mixinStandardHelpOptions = true,
053    versionProvider = VersionProvider.class,
054    description = "Display workflow and step descriptions."
055)
056public class DescribeCLI implements Callable<Integer> {
057
058    @Option(
059        names = {"-d", "--dir"},
060        description = "Base directory. Defaults to current directory.",
061        defaultValue = "."
062    )
063    private File baseDir;
064
065    @Option(
066        names = {"-w", "--workflow"},
067        description = "Workflow file path relative to -d (required)",
068        required = true
069    )
070    private String workflowPath;
071
072    @Option(
073        names = {"-o", "--overlay"},
074        description = "Overlay directory containing overlay-conf.yaml"
075    )
076    private File overlayDir;
077
078    @Option(
079        names = {"--steps", "--notes"},
080        description = "Also display note/description of each step"
081    )
082    private boolean showSteps;
083
084    @Override
085    public Integer call() {
086        // Resolve workflow file (try extensions if needed)
087        File resolvedFile = resolveWorkflowFile(new File(baseDir, workflowPath));
088        if (resolvedFile == null || !resolvedFile.isFile()) {
089            System.err.println("Workflow file not found: " + workflowPath);
090            return 1;
091        }
092
093        // Load YAML (with overlay if specified)
094        Map<String, Object> yaml;
095        if (overlayDir != null) {
096            yaml = loadYamlWithOverlay(resolvedFile);
097        } else {
098            yaml = loadYamlFile(resolvedFile);
099        }
100
101        if (yaml == null) {
102            System.err.println("Failed to load workflow: " + resolvedFile);
103            return 1;
104        }
105
106        // Print workflow description
107        printWorkflowDescription(resolvedFile, yaml);
108
109        return 0;
110    }
111
112    /**
113     * Resolves the workflow file, trying extensions if the file doesn't exist.
114     */
115    private File resolveWorkflowFile(File file) {
116        if (file.isFile()) {
117            return file;
118        }
119        // Try adding extensions
120        String[] extensions = {".yaml", ".yml", ".json", ".xml"};
121        for (String ext : extensions) {
122            File candidate = new File(file.getPath() + ext);
123            if (candidate.isFile()) {
124                return candidate;
125            }
126        }
127        return null;
128    }
129
130    /**
131     * Prints workflow description.
132     */
133    @SuppressWarnings("unchecked")
134    private void printWorkflowDescription(File file, Map<String, Object> yaml) {
135        String name = (String) yaml.getOrDefault("name", "(unnamed)");
136        String description = (String) yaml.get("description");
137
138        System.out.println("Workflow: " + name);
139        System.out.println("File: " + file.getAbsolutePath());
140        if (overlayDir != null) {
141            System.out.println("Overlay: " + overlayDir.getAbsolutePath());
142        }
143        System.out.println();
144
145        // Workflow-level description
146        System.out.println("Description:");
147        if (description != null && !description.isBlank()) {
148            for (String line : description.split("\n")) {
149                System.out.println("  " + line);
150            }
151        } else {
152            System.out.println("  (no description)");
153        }
154
155        // Step descriptions (if --steps flag is set)
156        if (showSteps) {
157            System.out.println();
158            System.out.println("Steps:");
159
160            List<Map<String, Object>> steps = (List<Map<String, Object>>) yaml.get("steps");
161            if (steps == null || steps.isEmpty()) {
162                System.out.println("  (no steps defined)");
163                return;
164            }
165
166            for (Map<String, Object> step : steps) {
167                List<String> states = (List<String>) step.get("states");
168                String label = (String) step.get("label");
169                String stepNote = (String) step.get("note");
170
171                String stateTransition = (states != null && states.size() >= 2)
172                    ? states.get(0) + " -> " + states.get(1)
173                    : "?";
174
175                String displayName = (label != null) ? label : "(unnamed)";
176
177                System.out.println();
178                System.out.println("  [" + stateTransition + "] " + displayName);
179                if (stepNote != null && !stepNote.isBlank()) {
180                    for (String line : stepNote.split("\n")) {
181                        System.out.println("    " + line);
182                    }
183                }
184            }
185        }
186    }
187
188    /**
189     * Loads a YAML file.
190     */
191    private Map<String, Object> loadYamlFile(File file) {
192        try (InputStream is = new FileInputStream(file)) {
193            org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml();
194            return yaml.load(is);
195        } catch (Exception e) {
196            System.err.println("Failed to load YAML file: " + file + " - " + e.getMessage());
197            return null;
198        }
199    }
200
201    /**
202     * Loads a YAML file with overlay applied.
203     */
204    private Map<String, Object> loadYamlWithOverlay(File workflowFile) {
205        try {
206            WorkflowKustomizer kustomizer = new WorkflowKustomizer();
207            Map<String, Map<String, Object>> workflows = kustomizer.build(overlayDir.toPath());
208
209            String targetName = workflowFile.getName();
210            for (Map.Entry<String, Map<String, Object>> entry : workflows.entrySet()) {
211                if (entry.getKey().equals(targetName)) {
212                    return entry.getValue();
213                }
214            }
215
216            // Fall back to raw file
217            return loadYamlFile(workflowFile);
218        } catch (Exception e) {
219            System.err.println("Failed to apply overlay, loading raw file: " + e.getMessage());
220            return loadYamlFile(workflowFile);
221        }
222    }
223
224}