OverviewWorkspaceMojo.java

package network.ike.plugin.ws;

import network.ike.workspace.Subproject;
import network.ike.workspace.Dependency;
import network.ike.workspace.WorkspaceGraph;
import network.ike.plugin.support.GoalReportBuilder;
import network.ike.plugin.ws.vcs.VcsOperations;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;

import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Consolidated workspace overview — manifest, graph, status, cascade.
 *
 * <p>Replaces the former separate {@code ws:dashboard}, {@code ws:status},
 * and {@code ws:graph} goals with a single command. Loads the manifest
 * once and presents four sections:
 *
 * <ol>
 *   <li><b>Manifest</b> — subproject count, consistency check</li>
 *   <li><b>Graph</b> — dependency order with direct dependencies</li>
 *   <li><b>Status</b> — branch, SHA, clean/uncommitted per subproject</li>
 *   <li><b>Cascade</b> — downstream rebuild impact of components with
 *       uncommitted changes</li>
 * </ol>
 *
 * <p>Use {@code -Dformat=dot} to output Graphviz DOT format instead
 * of the overview (delegates to graph rendering).
 *
 * <pre>{@code
 * mvn ws:overview
 * mvn ws:overview -Dformat=dot
 * }</pre>
 */
@Mojo(name = "overview", projectRequired = false, aggregator = true)
public class OverviewWorkspaceMojo extends AbstractWorkspaceMojo {

    /** Creates this goal instance. */
    public OverviewWorkspaceMojo() {}

    /** Output format: "overview" (default) or "dot" (Graphviz DOT). */
    @Parameter(property = "format", defaultValue = "overview")
    String format;

    @Override
    protected WorkspaceReportSpec runGoal() throws MojoException {
        WorkspaceGraph graph = loadGraph();

        // DOT mode — delegate to graph-only output
        if ("dot".equalsIgnoreCase(format)) {
            printDot(graph);
            return new WorkspaceReportSpec(WsGoal.OVERVIEW,
                    "DOT format emitted to console — no report generated.\n");
        }

        File root = workspaceRoot();

        getLog().info("");
        getLog().info(header("Overview"));
        getLog().info("══════════════════════════════════════════════════════════════");

        // ── Section 1: Manifest ─────────────────────────────────────
        List<String> errors = graph.verify();
        getLog().info("");
        if (errors.isEmpty()) {
            getLog().info(Ansi.green("  ✓ ") + "Manifest: "
                    + graph.manifest().subprojects().size()
                    + " components — consistent");
        } else {
            getLog().warn(Ansi.red("  ✗ ") + "Manifest: "
                    + errors.size() + " error(s)");
            for (String error : errors) {
                getLog().warn("    " + error);
            }
        }

        // ── Section 2: Dependency Graph ─────────────────────────────
        List<String> sorted = graph.topologicalSort();
        getLog().info("");
        getLog().info("  Graph (dependency order)");
        getLog().info("  ──────────────────────────────────────────────────────");

        List<String[]> graphRows = new ArrayList<>();
        for (int i = 0; i < sorted.size(); i++) {
            String name = sorted.get(i);
            Subproject sub = graph.manifest().subprojects().get(name);
            String deps = sub.dependsOn().isEmpty() ? "—"
                    : sub.dependsOn().stream()
                        .map(Dependency::subproject)
                        .collect(Collectors.joining(", "));

            getLog().info(String.format("  %2d. %-24s → %s",
                    i + 1, name, deps));
            graphRows.add(new String[]{String.valueOf(i + 1), name, deps});
        }

        // ── Section 3: Subproject Status ─────────────────────────────
        Set<String> targets = graph.manifest().subprojects().keySet();

        getLog().info("");
        getLog().info("  Status");
        getLog().info("  ──────────────────────────────────────────────────────");
        getLog().info(String.format("  %-24s %-24s %-8s %s",
                "COMPONENT", "BRANCH", "SHA", ""));

        List<String> modifiedComponents = new ArrayList<>();
        List<String[]> statusRows = new ArrayList<>();
        int cloned = 0;
        int notCloned = 0;

        for (String name : targets) {
            Subproject sub = graph.manifest().subprojects().get(name);
            File dir = new File(root, name);

            if (!dir.exists()) {
                notCloned++;
                getLog().info(String.format("  %-24s %-24s %-8s %s",
                        name, "—", "—", "not cloned"));
                statusRows.add(new String[]{name, "—", "—", "not cloned"});
                continue;
            }

            cloned++;
            String branch = gitBranch(dir);
            String sha = gitShortSha(dir);
            String status = gitStatus(dir);

            String marker;
            if (status.isEmpty()) {
                marker = Ansi.green("✓");
            } else {
                long count = status.lines().count();
                marker = Ansi.red("✗") + " " + count + " changed";
                modifiedComponents.add(name);
            }

            String branchCol = branch;
            if (sub.branch() != null && !branch.equals(sub.branch())) {
                branchCol = branch + Ansi.yellow(" ⚠");
            }

            getLog().info(String.format("  %-24s %-24s %-8s %s",
                    name, branchCol, sha, marker));
            statusRows.add(new String[]{name, branch, sha,
                    status.isEmpty() ? "clean"
                            : "uncommitted (" + status.lines().count() + " files)"});
        }

        getLog().info("");
        getLog().info("  " + cloned + " cloned, " + notCloned + " not cloned, "
                + modifiedComponents.size() + " with uncommitted changes");

        // ── Section 4: Feature Branch Divergence ────────────────────
        // If any subproject is on a feature branch, show how far it has
        // diverged from main — helps developers keep long-lived branches
        // up to date.
        List<String[]> divergenceRows = new ArrayList<>();
        boolean anyOnFeature = false;
        int warnThreshold = 20;

        for (String name : targets) {
            File dir = new File(root, name);
            if (!dir.exists() || !new File(dir, ".git").exists()) continue;

            String branch = gitBranch(dir);
            if (!branch.startsWith("feature/")) continue;

            anyOnFeature = true;
            try {
                List<String> behind = VcsOperations.commitLog(dir, branch, "main");
                List<String> ahead = VcsOperations.commitLog(dir, "main", branch);

                String divergence;
                String marker;
                if (behind.isEmpty()) {
                    divergence = "up to date";
                    marker = Ansi.green("✓");
                } else if (behind.size() >= warnThreshold) {
                    divergence = behind.size() + " behind, "
                            + ahead.size() + " ahead";
                    marker = Ansi.red("⚠") + " consider ws:update-feature";
                } else {
                    divergence = behind.size() + " behind, "
                            + ahead.size() + " ahead";
                    marker = Ansi.yellow("·");
                }

                divergenceRows.add(new String[]{name, branch,
                        String.valueOf(behind.size()),
                        String.valueOf(ahead.size()), divergence});

                if (!anyOnFeature) continue; // skip printing header until first
            } catch (MojoException e) {
                // Can't determine divergence (no main branch, etc.) — skip
                divergenceRows.add(new String[]{name, branch,
                        "?", "?", "unknown"});
            }
        }

        if (anyOnFeature) {
            getLog().info("");
            getLog().info("  Feature Branch Divergence (from main)");
            getLog().info("  ──────────────────────────────────────────────────────");

            for (String[] row : divergenceRows) {
                String name = row[0];
                int behind = "?".equals(row[2]) ? -1 : Integer.parseInt(row[2]);
                int ahead = "?".equals(row[3]) ? -1 : Integer.parseInt(row[3]);

                String marker;
                String detail;
                if (behind == 0) {
                    marker = Ansi.green("✓");
                    detail = "up to date";
                } else if (behind < 0) {
                    marker = "?";
                    detail = "unknown";
                } else if (behind >= warnThreshold) {
                    marker = Ansi.red("⚠");
                    detail = behind + " commit(s) behind main, "
                            + ahead + " ahead — consider ws:update-feature";
                } else {
                    marker = Ansi.yellow("·");
                    detail = behind + " commit(s) behind main, " + ahead + " ahead";
                }

                getLog().info(String.format("  %-24s %s %s",
                        name, marker, detail));
            }
        }

        // ── Section 5: Cascade ──────────────────────────────────────
        List<String[]> cascadeRows = new ArrayList<>();
        if (!modifiedComponents.isEmpty()) {
            Set<String> allAffected = new LinkedHashSet<>();
            for (String sub : modifiedComponents) {
                allAffected.addAll(graph.cascade(sub));
            }
            allAffected.removeAll(modifiedComponents);

            if (!allAffected.isEmpty()) {
                getLog().info("");
                getLog().info("  Cascade — components needing rebuild:");
                getLog().info("  ──────────────────────────────────────────────────────");

                List<String> buildOrder = graph.topologicalSort(
                        new LinkedHashSet<>(allAffected));
                for (String name : buildOrder) {
                    List<String> triggeredBy = new ArrayList<>();
                    Subproject sub = graph.manifest().subprojects().get(name);
                    if (sub != null) {
                        for (Dependency dep : sub.dependsOn()) {
                            if (modifiedComponents.contains(dep.subproject())
                                    || allAffected.contains(dep.subproject())) {
                                triggeredBy.add(dep.subproject());
                            }
                        }
                    }
                    String triggers = String.join(", ", triggeredBy);
                    getLog().info("    " + name + " ← " + triggers);
                    cascadeRows.add(new String[]{name, triggers});
                }
            }
        }

        getLog().info("");

        // Structured markdown report
        return new WorkspaceReportSpec(WsGoal.OVERVIEW, buildMarkdownReport(
                errors, graphRows, statusRows, divergenceRows, cascadeRows,
                cloned, notCloned, modifiedComponents.size(), graph));
    }

    // ── Markdown report ─────────────────────────────────────────────

    private String buildMarkdownReport(List<String> manifestErrors,
                                        List<String[]> graphRows,
                                        List<String[]> statusRows,
                                        List<String[]> divergenceRows,
                                        List<String[]> cascadeRows,
                                        int cloned, int notCloned,
                                        int modified,
                                        WorkspaceGraph graph) {
        GoalReportBuilder report = new GoalReportBuilder();

        report.paragraph(manifestErrors.isEmpty()
                ? "**Manifest:** consistent."
                : "**Manifest:** " + manifestErrors.size() + " error(s).");

        // GraphViz dependency graph — IKE-DIAGRAMS.md mandates
        // GraphViz for dependency graphs (IKE-Network/ike-issues#406).
        report.section("Dependency Graph")
                .table(List.of("#", "Subproject", "Dependencies"), graphRows)
                .raw(DotGraphSupport.buildDotReportBlock(graph));

        report.section("Status")
                .paragraph(cloned + " cloned, " + notCloned + " not cloned, "
                        + modified + " with uncommitted changes.")
                .table(List.of("Subproject", "Branch", "SHA", "Status"),
                        statusRows);

        if (!divergenceRows.isEmpty()) {
            report.section("Feature Branch Divergence")
                    .table(List.of("Subproject", "Branch", "Behind", "Ahead",
                            "Status"), divergenceRows);
        }

        if (!cascadeRows.isEmpty()) {
            report.section("Cascade")
                    .table(List.of("Subproject", "Triggered By"), cascadeRows);
        }

        return report.build();
    }

    // ── DOT output ──────────────────────────────────────────────────

    private void printDot(WorkspaceGraph graph) {
        for (String line
                : DotGraphSupport.dotFromGraph(graph).split("\n")) {
            getLog().info(line);
        }
    }
}