ContextCompareService.java

package com.taxonomy.versioning.service;

import com.taxonomy.dsl.diff.ModelDiff;
import com.taxonomy.dsl.model.ArchitectureElement;
import com.taxonomy.dsl.model.ArchitectureRelation;
import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.dto.ContextComparison;
import com.taxonomy.dto.ContextComparison.DiffSummary;
import com.taxonomy.dto.ContextRef;
import com.taxonomy.dto.SemanticChange;
import com.taxonomy.workspace.service.WorkspaceContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Compares two architecture contexts and produces a semantic diff.
 *
 * <p>Builds on the existing {@link DslGitRepository#diffBetween(String, String)}
 * infrastructure and enriches the result with human-readable change descriptions.
 */
@Service
public class ContextCompareService {

    private static final Logger log = LoggerFactory.getLogger(ContextCompareService.class);

    private final DslGitRepositoryFactory repositoryFactory;

    public ContextCompareService(DslGitRepositoryFactory repositoryFactory) {
        this.repositoryFactory = repositoryFactory;
    }

    /**
     * Resolve the Git repository for the given workspace context.
     *
     * @param ctx the workspace context (use {@link WorkspaceContext#SHARED}
     *            for the system repository)
     * @return the resolved DslGitRepository
     */
    private DslGitRepository resolveRepository(WorkspaceContext ctx) {
        return repositoryFactory.resolveRepository(ctx);
    }

    /**
     * Compare two contexts identified by their commit IDs.
     *
     * <p>Uses the system repository (SHARED context). Use
     * {@link #compareContexts(ContextRef, ContextRef, WorkspaceContext)} for workspace-aware resolution.
     *
     * @param left  the left (source/older) context
     * @param right the right (target/newer) context
     * @return the comparison result
     * @throws IOException if Git operations fail
     */
    public ContextComparison compareContexts(ContextRef left, ContextRef right) throws IOException {
        return compareContexts(left, right, WorkspaceContext.SHARED);
    }

    /**
     * Compare two contexts identified by their commit IDs.
     *
     * @param left  the left (source/older) context
     * @param right the right (target/newer) context
     * @param ctx   the workspace context for repository resolution
     * @return the comparison result
     * @throws IOException if Git operations fail
     */
    public ContextComparison compareContexts(ContextRef left, ContextRef right,
                                             WorkspaceContext ctx) throws IOException {
        DslGitRepository repo = resolveRepository(ctx);
        String leftCommit = resolveCommit(repo, left);
        String rightCommit = resolveCommit(repo, right);

        if (leftCommit == null || rightCommit == null) {
            return new ContextComparison(left, right,
                    new DiffSummary(0, 0, 0, 0, 0, 0),
                    List.of(), null);
        }

        ModelDiff diff = repo.diffBetween(leftCommit, rightCommit);
        DiffSummary summary = buildDiffSummary(diff);
        List<SemanticChange> changes = buildSemanticChanges(diff);

        String rawDiff = null;
        try {
            rawDiff = repo.textDiff(leftCommit, rightCommit);
        } catch (IOException e) {
            log.warn("Could not generate raw text diff: {}", e.getMessage());
        }

        return new ContextComparison(left, right, summary, changes, rawDiff);
    }

    /**
     * Compare two branches at their HEAD commits.
     *
     * <p>Uses the system repository (SHARED context). Use
     * {@link #compareBranches(ContextRef, ContextRef, WorkspaceContext)} for workspace-aware resolution.
     *
     * @param left  the left context
     * @param right the right context
     * @return the comparison result
     * @throws IOException if Git operations fail
     */
    public ContextComparison compareBranches(ContextRef left, ContextRef right) throws IOException {
        return compareBranches(left, right, WorkspaceContext.SHARED);
    }

    /**
     * Compare two branches at their HEAD commits.
     *
     * @param left  the left context
     * @param right the right context
     * @param ctx   the workspace context for repository resolution
     * @return the comparison result
     * @throws IOException if Git operations fail
     */
    public ContextComparison compareBranches(ContextRef left, ContextRef right,
                                             WorkspaceContext ctx) throws IOException {
        DslGitRepository repo = resolveRepository(ctx);
        ModelDiff diff = repo.diffBranches(left.branch(), right.branch());
        DiffSummary summary = buildDiffSummary(diff);
        List<SemanticChange> changes = buildSemanticChanges(diff);

        String rawDiff = null;
        try {
            String leftCommit = repo.getHeadCommit(left.branch());
            String rightCommit = repo.getHeadCommit(right.branch());
            if (leftCommit != null && rightCommit != null) {
                rawDiff = repo.textDiff(leftCommit, rightCommit);
            }
        } catch (Exception e) {
            log.debug("Could not generate raw diff for branch compare: {}", e.getMessage());
        }

        return new ContextComparison(left, right, summary, changes, rawDiff);
    }

    // ── Internal helpers ────────────────────────────────────────────

    DiffSummary buildDiffSummary(ModelDiff diff) {
        return new DiffSummary(
                diff.addedElements().size(),
                diff.changedElements().size(),
                diff.removedElements().size(),
                diff.addedRelations().size(),
                diff.changedRelations().size(),
                diff.removedRelations().size()
        );
    }

    List<SemanticChange> buildSemanticChanges(ModelDiff diff) {
        List<SemanticChange> changes = new ArrayList<>();

        for (ArchitectureElement el : diff.addedElements()) {
            changes.add(new SemanticChange(
                    "ADD", "ELEMENT", el.getId(),
                    "Element " + el.getId() + " added (" + el.getType() + ")",
                    null, el.getTitle()));
        }

        for (ArchitectureElement el : diff.removedElements()) {
            changes.add(new SemanticChange(
                    "REMOVE", "ELEMENT", el.getId(),
                    "Element " + el.getId() + " removed (" + el.getType() + ")",
                    el.getTitle(), null));
        }

        for (ModelDiff.ElementChange change : diff.changedElements()) {
            changes.add(new SemanticChange(
                    "MODIFY", "ELEMENT", change.after().getId(),
                    "Element " + change.after().getId() + " modified",
                    change.before().getTitle(),
                    change.after().getTitle()));
        }

        for (ArchitectureRelation rel : diff.addedRelations()) {
            String key = rel.getSourceId() + " " + rel.getRelationType() + " " + rel.getTargetId();
            changes.add(new SemanticChange(
                    "ADD", "RELATION", key,
                    "Relation added: " + key,
                    null, key));
        }

        for (ArchitectureRelation rel : diff.removedRelations()) {
            String key = rel.getSourceId() + " " + rel.getRelationType() + " " + rel.getTargetId();
            changes.add(new SemanticChange(
                    "REMOVE", "RELATION", key,
                    "Relation removed: " + key,
                    key, null));
        }

        for (ModelDiff.RelationChange change : diff.changedRelations()) {
            String key = change.after().getSourceId() + " "
                    + change.after().getRelationType() + " " + change.after().getTargetId();
            changes.add(new SemanticChange(
                    "MODIFY", "RELATION", key,
                    "Relation modified: " + key,
                    change.before().getStatus(),
                    change.after().getStatus()));
        }

        return changes;
    }

    private String resolveCommit(DslGitRepository repo, ContextRef ctx) {
        if (ctx.commitId() != null) {
            return ctx.commitId();
        }
        try {
            return repo.getHeadCommit(ctx.branch());
        } catch (IOException e) {
            log.warn("Could not resolve HEAD for branch '{}': {}", ctx.branch(), e.getMessage());
            return null;
        }
    }
}