ProjectAnalysisController.java

package com.taxonomy.portfolio.controller;

import com.taxonomy.portfolio.dto.PortfolioDtos.AnalysisJobView;
import com.taxonomy.portfolio.dto.PortfolioDtos.AnalyzeProjectRequest;
import com.taxonomy.portfolio.dto.PortfolioDtos.ElementMappingView;
import com.taxonomy.portfolio.dto.PortfolioDtos.RelationMappingView;
import com.taxonomy.portfolio.dto.PortfolioDtos.ReviewElementMappingRequest;
import com.taxonomy.portfolio.dto.PortfolioDtos.ReviewRelationMappingRequest;
import com.taxonomy.portfolio.dto.PortfolioDtos.SnapshotDetail;
import com.taxonomy.portfolio.dto.PortfolioDtos.SnapshotDiff;
import com.taxonomy.portfolio.dto.PortfolioDtos.SnapshotSummary;
import com.taxonomy.portfolio.service.PortfolioAnalysisPersistenceService;
import com.taxonomy.portfolio.service.ProjectRequirementAnalysisService;
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.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;
import java.util.List;

/** Requirement-separated analysis jobs and immutable snapshot history. */
@RestController
@RequestMapping("/api/projects/{projectId}")
@Tag(name = "Project Requirement Analysis")
public class ProjectAnalysisController {

    private final ProjectRequirementAnalysisService analysisService;
    private final PortfolioAnalysisPersistenceService persistenceService;
    private final WorkspaceResolver workspaceResolver;

    public ProjectAnalysisController(ProjectRequirementAnalysisService analysisService,
                                     PortfolioAnalysisPersistenceService persistenceService,
                                     WorkspaceResolver workspaceResolver) {
        this.analysisService = analysisService;
        this.persistenceService = persistenceService;
        this.workspaceResolver = workspaceResolver;
    }

    @PostMapping("/analyses")
    @Operation(summary = "Queue independent analyses for selected or all requirements")
    public ResponseEntity<AnalysisJobView> analyzeProject(@PathVariable Long projectId,
                                                          @RequestBody AnalyzeProjectRequest request) {
        RequestScope scope = scope();
        AnalysisJobView job = analysisService.enqueueProject(
                projectId, request, scope.username(), scope.context());
        return accepted(projectId, job);
    }

    @PostMapping("/requirements/{requirementId}/analyses")
    @Operation(summary = "Queue one requirement analysis and immutable snapshot")
    public ResponseEntity<AnalysisJobView> analyzeRequirement(
            @PathVariable Long projectId,
            @PathVariable Long requirementId,
            @RequestBody(required = false) AnalyzeProjectRequest request) {
        RequestScope scope = scope();
        AnalyzeProjectRequest effective = request != null
                ? request : new AnalyzeProjectRequest(List.of(requirementId), false, null, null, null);
        AnalysisJobView job = analysisService.enqueueRequirement(
                projectId,
                requirementId,
                effective.provider(),
                effective.maxArchitectureNodes(),
                effective.idempotencyKey(),
                scope.username(),
                scope.context());
        return accepted(projectId, job);
    }

    @GetMapping("/analysis-jobs")
    @Operation(summary = "List project analysis jobs")
    public List<AnalysisJobView> listJobs(@PathVariable Long projectId) {
        RequestScope scope = scope();
        return analysisService.listJobs(projectId, scope.username(), scope.context());
    }

    @GetMapping("/analysis-jobs/{jobId}")
    @Operation(summary = "Read analysis job and item status")
    public AnalysisJobView getJob(@PathVariable Long projectId,
                                  @PathVariable String jobId) {
        RequestScope scope = scope();
        return analysisService.getJob(jobId, projectId, scope.username(), scope.context());
    }

    @PostMapping("/analysis-jobs/{jobId}/retry-failed")
    @Operation(summary = "Queue failed or expired requirement analyses for retry")
    public ResponseEntity<AnalysisJobView> retryFailed(@PathVariable Long projectId,
                                                       @PathVariable String jobId) {
        RequestScope scope = scope();
        AnalysisJobView job = analysisService.enqueueRetryFailed(
                jobId, projectId, scope.username(), scope.context());
        return accepted(projectId, job);
    }

    @GetMapping("/requirements/{requirementId}/snapshots")
    @Operation(summary = "List immutable analysis snapshots for a requirement")
    public List<SnapshotSummary> listSnapshots(@PathVariable Long projectId,
                                               @PathVariable Long requirementId) {
        RequestScope scope = scope();
        return analysisService.listSnapshots(
                projectId, requirementId, scope.username(), scope.context());
    }

    @GetMapping("/snapshots/{snapshotId}")
    @Operation(summary = "Replay one immutable analysis snapshot")
    public SnapshotDetail getSnapshot(@PathVariable Long projectId,
                                      @PathVariable String snapshotId) {
        RequestScope scope = scope();
        return analysisService.getSnapshot(projectId, snapshotId, scope.username(), scope.context());
    }

    @GetMapping("/snapshots/diff")
    @Operation(summary = "Compare two analysis snapshots semantically")
    public SnapshotDiff diffSnapshots(@PathVariable Long projectId,
                                      @RequestParam String older,
                                      @RequestParam String newer) {
        RequestScope scope = scope();
        return analysisService.diffSnapshots(
                projectId, older, newer, scope.username(), scope.context());
    }

    @PatchMapping("/analysis-mappings/elements/{mappingId}")
    @Operation(summary = "Review a requirement-to-taxonomy mapping and classify the action")
    public ElementMappingView reviewElementMapping(
            @PathVariable Long projectId,
            @PathVariable Long mappingId,
            @RequestBody ReviewElementMappingRequest request) {
        RequestScope scope = scope();
        return persistenceService.reviewElementMapping(
                projectId, mappingId, request, scope.username(), scope.context());
    }

    @PatchMapping("/analysis-mappings/relations/{mappingId}")
    @Operation(summary = "Review a requirement-derived architecture relation")
    public RelationMappingView reviewRelationMapping(
            @PathVariable Long projectId,
            @PathVariable Long mappingId,
            @RequestBody ReviewRelationMappingRequest request) {
        RequestScope scope = scope();
        return persistenceService.reviewRelationMapping(
                projectId, mappingId, request, scope.username(), scope.context());
    }

    private static ResponseEntity<AnalysisJobView> accepted(Long projectId, AnalysisJobView job) {
        URI location = URI.create(
                "/api/projects/" + projectId + "/analysis-jobs/" + job.id());
        return ResponseEntity.accepted().location(location).body(job);
    }

    private RequestScope scope() {
        return new RequestScope(
                workspaceResolver.resolveCurrentUsername(),
                workspaceResolver.resolveCurrentContext());
    }

    private record RequestScope(String username, WorkspaceContext context) {
    }
}