ContextNavigationController.java

package com.taxonomy.versioning.controller;

import com.taxonomy.dto.ContextComparison;
import com.taxonomy.dto.ContextHistoryEntry;
import com.taxonomy.dto.ContextRef;
import com.taxonomy.dto.SemanticChange;
import com.taxonomy.dto.TransferConflict;
import com.taxonomy.dto.TransferSelection;
import com.taxonomy.versioning.service.ContextCompareService;
import com.taxonomy.versioning.service.ContextNavigationService;
import com.taxonomy.versioning.service.SelectiveTransferService;
import com.taxonomy.workspace.service.WorkspaceContext;
import com.taxonomy.workspace.service.WorkspaceResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * REST API for architecture context navigation.
 *
 * <p>Provides browser-like navigation through architecture versions:
 * open read-only snapshots, switch branches, compare contexts, and
 * selectively transfer elements between versions.
 *
 * <p>All navigation state is isolated per authenticated user via the
 * workspace manager. Each user has their own context, history, and
 * navigation trail.
 */
@RestController
@RequestMapping("/api/context")
@Tag(name = "Context Navigation")
public class ContextNavigationController {

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

    private final ContextNavigationService navigationService;
    private final ContextCompareService compareService;
    private final SelectiveTransferService transferService;
    private final WorkspaceResolver workspaceResolver;

    public ContextNavigationController(ContextNavigationService navigationService,
                                       ContextCompareService compareService,
                                       SelectiveTransferService transferService,
                                       WorkspaceResolver workspaceResolver) {
        this.navigationService = navigationService;
        this.compareService = compareService;
        this.transferService = transferService;
        this.workspaceResolver = workspaceResolver;
    }

    /**
     * Resolve the current workspace context from the authenticated user.
     * Falls back to {@link WorkspaceContext#SHARED} if resolution fails.
     */
    private WorkspaceContext resolveContext() {
        try {
            return workspaceResolver.resolveCurrentContext();
        } catch (Exception e) {
            return WorkspaceContext.SHARED;
        }
    }

    // ── Phase 1: Context Navigation ─────────────────────────────────

    @GetMapping("/current")
    @Operation(summary = "Get the current architecture context",
            description = "Returns the active context including branch, commit, mode, and origin info.")
    public ResponseEntity<ContextRef> getCurrentContext() {
        String user = workspaceResolver.resolveCurrentUsername();
        return ResponseEntity.ok(navigationService.getCurrentContext(user));
    }

    @PostMapping("/open")
    @Operation(summary = "Open a context (read-only or editable)",
            description = "Opens a specific branch/commit as a new context. " +
                    "If readOnly is true, write operations will be blocked.")
    public ResponseEntity<ContextRef> openContext(
            @RequestParam(defaultValue = "draft") String branch,
            @RequestParam(required = false) String commitId,
            @RequestParam(defaultValue = "true") boolean readOnly,
            @RequestParam(required = false) String searchQuery,
            @RequestParam(required = false) String elementId) {
        String user = workspaceResolver.resolveCurrentUsername();
        WorkspaceContext ctx = resolveContext();
        if (readOnly) {
            return ResponseEntity.ok(
                    navigationService.openReadOnly(user, branch, commitId, ctx, searchQuery, elementId));
        } else {
            return ResponseEntity.ok(
                    navigationService.switchContext(user, branch, commitId, ctx));
        }
    }

    @PostMapping("/return-to-origin")
    @Operation(summary = "Return to the origin context",
            description = "Navigates back to the context from which the current context was opened.")
    public ResponseEntity<ContextRef> returnToOrigin() {
        String user = workspaceResolver.resolveCurrentUsername();
        return ResponseEntity.ok(navigationService.returnToOrigin(user));
    }

    @PostMapping("/back")
    @Operation(summary = "Go one step back in navigation history",
            description = "Like the browser back button — returns to the previous context.")
    public ResponseEntity<ContextRef> back() {
        String user = workspaceResolver.resolveCurrentUsername();
        return ResponseEntity.ok(navigationService.back(user));
    }

    @GetMapping("/history")
    @Operation(summary = "Get the navigation history",
            description = "Returns the list of context navigations (newest last).")
    public ResponseEntity<List<ContextHistoryEntry>> getHistory() {
        String user = workspaceResolver.resolveCurrentUsername();
        return ResponseEntity.ok(navigationService.getHistory(user));
    }

    @PostMapping("/variant")
    @Operation(summary = "Create a new branch variant from the current context",
            description = "Creates a new Git branch from the current context and switches to it.")
    public ResponseEntity<Map<String, Object>> createVariant(
            @RequestParam String name) {
        try {
            String user = workspaceResolver.resolveCurrentUsername();
            ContextRef variant = navigationService.createVariantFromCurrent(user, name, resolveContext());
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("context", variant);
            result.put("branch", variant.branch());
            return ResponseEntity.ok(result);
        } catch (IOException e) {
            log.error("Failed to create variant '{}'", name, e);
            return ResponseEntity.internalServerError().body(
                    Map.of("error", "Failed to create variant: " + e.getMessage()));
        }
    }

    // ── Phase 3: Compare ────────────────────────────────────────────

    @GetMapping("/compare")
    @Operation(summary = "Compare two architecture contexts",
            description = "Returns a semantic diff between two contexts identified by " +
                    "branch/commit pairs. Includes summary counts, individual changes, " +
                    "and optional raw DSL diff.")
    public ResponseEntity<ContextComparison> compare(
            @RequestParam String leftBranch,
            @RequestParam(required = false) String leftCommit,
            @RequestParam String rightBranch,
            @RequestParam(required = false) String rightCommit,
            @RequestParam(required = false) Set<String> filter) {
        try {
            ContextRef left = new ContextRef(
                    null, leftBranch, leftCommit, null, null,
                    null, null, null, null, null, false);
            ContextRef right = new ContextRef(
                    null, rightBranch, rightCommit, null, null,
                    null, null, null, null, null, false);

            ContextComparison comparison;
            if (leftCommit != null || rightCommit != null) {
                comparison = compareService.compareContexts(left, right, resolveContext());
            } else {
                comparison = compareService.compareBranches(left, right, resolveContext());
            }
            return ResponseEntity.ok(applyFilter(comparison, filter));
        } catch (IOException e) {
            log.error("Compare failed", e);
            return ResponseEntity.internalServerError().build();
        }
    }

    // ── Phase 4: Selective Transfer ─────────────────────────────────

    @PostMapping("/copy-back/preview")
    @Operation(summary = "Preview a selective transfer",
            description = "Shows what would happen if the selected elements and relations " +
                    "were transferred from source to target context, including conflicts.")
    public ResponseEntity<Map<String, Object>> previewTransfer(
            @RequestBody TransferSelection selection) {
        try {
            List<TransferConflict> conflicts = transferService.previewTransfer(selection, resolveContext());
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("conflicts", conflicts);
            result.put("hasConflicts", !conflicts.isEmpty());
            result.put("selectedElements", selection.selectedElementIds().size());
            result.put("selectedRelations", selection.selectedRelationIds().size());
            return ResponseEntity.ok(result);
        } catch (IOException e) {
            log.error("Transfer preview failed", e);
            return ResponseEntity.internalServerError().body(
                    Map.of("error", "Transfer preview failed: " + e.getMessage()));
        }
    }

    @PostMapping("/copy-back/apply")
    @Operation(summary = "Apply a selective transfer",
            description = "Transfers the selected elements and relations from the source " +
                    "context into the target context, creating a new commit.")
    public ResponseEntity<Map<String, Object>> applyTransfer(
            @RequestBody TransferSelection selection) {
        try {
            String commitId = transferService.applyTransfer(selection);
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("commitId", commitId);
            result.put("success", true);
            return ResponseEntity.ok(result);
        } catch (IOException e) {
            log.error("Transfer failed", e);
            return ResponseEntity.internalServerError().body(
                    Map.of("error", "Transfer failed: " + e.getMessage()));
        }
    }

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

    private ContextComparison applyFilter(ContextComparison comparison, Set<String> filter) {
        if (filter == null || filter.isEmpty()) {
            return comparison;
        }
        List<SemanticChange> filtered = comparison.changes().stream()
                .filter(c -> {
                    if (filter.contains("elements") && "ELEMENT".equals(c.category())) return true;
                    if (filter.contains("relations") && "RELATION".equals(c.category())) return true;
                    return false;
                })
                .toList();
        return new ContextComparison(comparison.left(), comparison.right(),
                comparison.summary(), filtered, comparison.rawDslDiff());
    }
}