WorkspaceManager.java

package com.taxonomy.workspace.service;

import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.dto.ContextRef;
import com.taxonomy.dto.WorkspaceInfo;
import com.taxonomy.workspace.model.RepositoryTopologyMode;
import com.taxonomy.workspace.model.UserWorkspace;
import com.taxonomy.workspace.model.WorkspaceProvisioningStatus;
import com.taxonomy.workspace.repository.UserWorkspaceRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Manages per-user workspace state for multi-user architecture editing.
 *
 * <p>Workspace metadata is persisted through {@link UserWorkspaceRepository},
 * while active navigation state stays in memory. When a
 * {@link DslGitRepositoryFactory} is available, each workspace uses its own
 * logical JGit repository name in the shared database.</p>
 */
@Service
public class WorkspaceManager {

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

    /** Default username used when no authentication context is available. */
    public static final String DEFAULT_USER = "anonymous";

    private final UserWorkspaceRepository workspaceRepository;
    private final int maxHistory;
    private final SystemRepositoryService systemRepositoryService;
    private final DslGitRepository gitRepository;
    private final DslGitRepositoryFactory repositoryFactory;

    /** Active workspace states keyed by workspace ID. */
    private final ConcurrentMap<String, UserWorkspaceState> activeWorkspaces =
            new ConcurrentHashMap<>();

    /** Maps a username to its currently active workspace ID. */
    private final ConcurrentMap<String, String> activeWorkspaceByUser =
            new ConcurrentHashMap<>();

    @Autowired
    public WorkspaceManager(
            UserWorkspaceRepository workspaceRepository,
            @Value("${taxonomy.context.max-history:50}") int maxHistory,
            SystemRepositoryService systemRepositoryService,
            DslGitRepositoryFactory repositoryFactory) {
        this.workspaceRepository = workspaceRepository;
        this.maxHistory = maxHistory;
        this.systemRepositoryService = systemRepositoryService;
        this.gitRepository = repositoryFactory.getSystemRepository();
        this.repositoryFactory = repositoryFactory;
    }

    /** Legacy constructor retained for unit tests using one caller-supplied repository. */
    public WorkspaceManager(
            UserWorkspaceRepository workspaceRepository,
            int maxHistory,
            SystemRepositoryService systemRepositoryService,
            DslGitRepository gitRepository) {
        this.workspaceRepository = workspaceRepository;
        this.maxHistory = maxHistory;
        this.systemRepositoryService = systemRepositoryService;
        this.gitRepository = gitRepository;
        this.repositoryFactory = null;
    }

    /** Return or create the user's default workspace state. */
    public UserWorkspaceState getOrCreateWorkspace(String username) {
        if (username == null || username.isBlank()) {
            username = DEFAULT_USER;
        }
        String user = username;

        String activeWorkspaceId = activeWorkspaceByUser.get(user);
        if (activeWorkspaceId != null) {
            UserWorkspaceState existing = activeWorkspaces.get(activeWorkspaceId);
            if (existing != null) {
                return existing;
            }
        }

        ensurePersistentWorkspace(user);

        UserWorkspace workspace = safeOptional(
                workspaceRepository.findByUsernameAndIsDefaultTrue(user));
        if (workspace != null && workspace.isArchived()) {
            workspace = null;
        }
        if (workspace == null) {
            workspace = safeOptional(workspaceRepository.findByUsernameAndSharedFalse(user));
            if (workspace != null && workspace.isArchived()) {
                workspace = null;
            }
        }

        String workspaceId = workspace != null
                ? workspace.getWorkspaceId()
                : user + "-workspace";
        UserWorkspaceState state = activeWorkspaces.computeIfAbsent(workspaceId, id -> {
            log.info("Creating workspace state for user '{}', workspaceId='{}'", user, id);
            return new UserWorkspaceState(user, maxHistory);
        });
        activeWorkspaceByUser.put(user, workspaceId);
        return state;
    }

    /** Return or create state for one explicitly selected workspace. */
    public UserWorkspaceState getOrCreateWorkspace(String username, String workspaceId) {
        if (username == null || username.isBlank()) {
            username = DEFAULT_USER;
        }
        String user = username;
        UserWorkspaceState state = activeWorkspaces.computeIfAbsent(workspaceId, id -> {
            log.info("Creating workspace state for user '{}', workspaceId='{}'", user, id);
            return new UserWorkspaceState(user, maxHistory);
        });
        activeWorkspaceByUser.put(user, workspaceId);
        return state;
    }

    /** Return the active in-memory workspace state for a user, or {@code null}. */
    public UserWorkspaceState getWorkspace(String username) {
        String user = username != null ? username : DEFAULT_USER;
        String activeWorkspaceId = activeWorkspaceByUser.get(user);
        return activeWorkspaceId != null ? activeWorkspaces.get(activeWorkspaceId) : null;
    }

    /** Return summaries of all currently active workspaces. */
    public List<WorkspaceInfo> listActiveWorkspaces() {
        return activeWorkspaces.values().stream()
                .map(this::toWorkspaceInfo)
                .toList();
    }

    /** Return the current workspace summary for a user. */
    public WorkspaceInfo getWorkspaceInfo(String username) {
        return toWorkspaceInfo(getOrCreateWorkspace(username));
    }

    /** Remove only a user's in-memory workspace state. */
    public void evictWorkspace(String username) {
        String activeWorkspaceId = activeWorkspaceByUser.remove(username);
        if (activeWorkspaceId == null) {
            return;
        }
        UserWorkspaceState removed = activeWorkspaces.remove(activeWorkspaceId);
        if (removed != null) {
            log.info("Evicted workspace state for user '{}', workspaceId='{}'",
                    username, activeWorkspaceId);
        }
    }

    /** Return the number of active in-memory workspaces. */
    public int getActiveWorkspaceCount() {
        return activeWorkspaces.size();
    }

    /** Find a user's persistent workspace, preferring the active one. */
    public UserWorkspace findUserWorkspace(String username) {
        try {
            UserWorkspace active = findActiveWorkspace(username);
            if (active != null) {
                return active;
            }
            return workspaceRepository.findByUsernameAndSharedFalse(username).orElse(null);
        } catch (Exception exception) {
            log.debug("Could not find workspace for '{}': {}",
                    username, exception.getMessage());
            return null;
        }
    }

    // ── Multi-workspace management ─────────────────────────────────

    /** Create a new, initially unprovisioned workspace. */
    public UserWorkspace createWorkspace(
            String username,
            String displayName,
            String description) {
        UserWorkspace workspace = new UserWorkspace();
        workspace.setWorkspaceId(UUID.randomUUID().toString());
        workspace.setUsername(username);
        workspace.setDisplayName(displayName);
        workspace.setDescription(description);
        workspace.setCurrentBranch("draft");
        workspace.setBaseBranch("draft");
        workspace.setShared(false);
        workspace.setArchived(false);
        workspace.setDefault(false);
        workspace.setProvisioningStatus(WorkspaceProvisioningStatus.NOT_PROVISIONED);
        workspace.setTopologyMode(RepositoryTopologyMode.INTERNAL_SHARED);
        workspace.setCreatedAt(Instant.now());
        workspace.setLastAccessedAt(Instant.now());
        workspaceRepository.save(workspace);
        log.info("Created new workspace '{}' for user '{}'", displayName, username);
        return workspace;
    }

    /** Switch the user's active workspace. */
    public UserWorkspace switchWorkspace(String username, String workspaceId) {
        UserWorkspace workspace = workspaceRepository.findByWorkspaceId(workspaceId)
                .orElseThrow(() ->
                        new IllegalArgumentException("Workspace not found: " + workspaceId));

        if (!workspace.getUsername().equals(username)) {
            throw new IllegalArgumentException(
                    "Cannot switch to workspace owned by another user");
        }
        if (workspace.isArchived()) {
            throw new IllegalArgumentException("Cannot switch to an archived workspace");
        }
        if (workspace.isShared()) {
            throw new IllegalArgumentException("Cannot switch to the shared workspace");
        }

        String oldWorkspaceId = activeWorkspaceByUser.get(username);
        if (oldWorkspaceId != null) {
            activeWorkspaces.remove(oldWorkspaceId);
        }

        activeWorkspaceByUser.put(username, workspaceId);
        activeWorkspaces.computeIfAbsent(
                workspaceId,
                id -> new UserWorkspaceState(username, maxHistory));

        workspace.setLastAccessedAt(Instant.now());
        workspaceRepository.save(workspace);
        log.info("User '{}' switched to workspace '{}'", username, workspaceId);
        return workspace;
    }

    /** Rename a workspace owned by the requesting user. */
    public UserWorkspace renameWorkspace(
            String username,
            String workspaceId,
            String newName) {
        UserWorkspace workspace = workspaceRepository.findByWorkspaceId(workspaceId)
                .orElseThrow(() ->
                        new IllegalArgumentException("Workspace not found: " + workspaceId));

        if (!workspace.getUsername().equals(username)) {
            throw new IllegalArgumentException(
                    "User '" + username + "' does not own workspace: " + workspaceId);
        }
        if (workspace.isArchived()) {
            throw new IllegalStateException(
                    "Archived workspace cannot be renamed: " + workspaceId);
        }
        if (workspace.isShared()) {
            throw new IllegalStateException(
                    "Shared workspace cannot be renamed: " + workspaceId);
        }

        workspace.setDisplayName(newName);
        workspaceRepository.save(workspace);
        log.info("User '{}' renamed workspace '{}' to '{}'",
                username, workspaceId, newName);
        return workspace;
    }

    /** Archive a non-default workspace without removing its Git history. */
    public UserWorkspace archiveWorkspace(String workspaceId, String username) {
        UserWorkspace workspace = workspaceRepository.findByWorkspaceId(workspaceId)
                .orElseThrow(() ->
                        new IllegalArgumentException("Workspace not found: " + workspaceId));

        if (!workspace.getUsername().equals(username)) {
            throw new IllegalArgumentException(
                    "Cannot archive workspace owned by another user");
        }
        if (workspace.isShared()) {
            throw new IllegalArgumentException("Cannot archive the shared workspace");
        }
        if (workspace.isDefault()) {
            throw new IllegalArgumentException("Cannot archive the default workspace");
        }

        workspace.setArchived(true);
        workspaceRepository.save(workspace);
        activeWorkspaces.remove(workspaceId);
        activeWorkspaceByUser.entrySet()
                .removeIf(entry -> entry.getValue().equals(workspaceId));
        log.info("Archived workspace '{}' for user '{}'", workspaceId, username);
        return workspace;
    }

    /**
     * Permanently delete a workspace and its isolated logical Git repository.
     *
     * <p>Storage deletion happens before metadata deletion. If the storage library
     * rejects or cannot complete deletion, the workspace row remains available for
     * diagnosis and retry rather than becoming an orphaned Git namespace.</p>
     */
    public void deleteWorkspace(String workspaceId, String username) {
        UserWorkspace workspace = workspaceRepository.findByWorkspaceId(workspaceId)
                .orElseThrow(() ->
                        new IllegalArgumentException("Workspace not found: " + workspaceId));

        if (!workspace.getUsername().equals(username)) {
            throw new IllegalArgumentException(
                    "Cannot delete workspace owned by another user");
        }
        if (workspace.isShared()) {
            throw new IllegalArgumentException("Cannot delete the shared workspace");
        }
        if (workspace.isDefault()) {
            throw new IllegalArgumentException("Cannot delete the default workspace");
        }

        activeWorkspaces.remove(workspaceId);
        activeWorkspaceByUser.entrySet()
                .removeIf(entry -> entry.getValue().equals(workspaceId));

        if (repositoryFactory != null) {
            repositoryFactory.deleteWorkspaceRepository(workspaceId);
        }
        workspaceRepository.delete(workspace);
        log.info("Deleted workspace '{}' and its logical Git repository for user '{}'",
                workspaceId, username);
    }

    /** Return all non-archived workspaces for a user. */
    public List<UserWorkspace> listUserWorkspaces(String username) {
        return workspaceRepository
                .findByUsernameAndArchivedFalseOrderByLastAccessedAtDesc(username);
    }

    /** Find the persistent entity for the user's active workspace. */
    public UserWorkspace findActiveWorkspace(String username) {
        String activeWorkspaceId = activeWorkspaceByUser.get(username);
        if (activeWorkspaceId == null) {
            return null;
        }
        try {
            return workspaceRepository.findByWorkspaceId(activeWorkspaceId).orElse(null);
        } catch (Exception exception) {
            log.debug("Could not find active workspace '{}' for '{}': {}",
                    activeWorkspaceId, username, exception.getMessage());
            return null;
        }
    }

    /** Return a workspace by ID, or {@code null} when it cannot be read. */
    public UserWorkspace getWorkspaceById(String workspaceId) {
        try {
            return workspaceRepository.findByWorkspaceId(workspaceId).orElse(null);
        } catch (Exception exception) {
            log.debug("Could not find workspace '{}': {}",
                    workspaceId, exception.getMessage());
            return null;
        }
    }

    /** Update the description of a non-archived workspace. */
    public UserWorkspace updateDescription(
            String username,
            String workspaceId,
            String description) {
        UserWorkspace workspace = workspaceRepository.findByWorkspaceId(workspaceId)
                .orElseThrow(() ->
                        new IllegalArgumentException("Workspace not found: " + workspaceId));

        if (!workspace.getUsername().equals(username)) {
            throw new IllegalArgumentException(
                    "User '" + username + "' does not own workspace: " + workspaceId);
        }
        if (workspace.isArchived()) {
            throw new IllegalStateException(
                    "Archived workspace cannot be updated: " + workspaceId);
        }

        workspace.setDescription(description);
        workspaceRepository.save(workspace);
        log.info("User '{}' updated description for workspace '{}'", username, workspaceId);
        return workspace;
    }

    // ── Provisioning ───────────────────────────────────────────────

    /** Lazily provision the active workspace repository. */
    public UserWorkspace provisionWorkspaceRepository(String username) {
        UserWorkspace workspace = null;
        String activeWorkspaceId = activeWorkspaceByUser.get(username);
        if (activeWorkspaceId != null) {
            workspace = workspaceRepository.findByWorkspaceId(activeWorkspaceId).orElse(null);
        }
        if (workspace == null) {
            workspace = workspaceRepository.findByUsernameAndSharedFalse(username)
                    .orElseThrow(() ->
                            new IllegalStateException("No workspace metadata for " + username));
        }

        if (workspace.getProvisioningStatus() == WorkspaceProvisioningStatus.READY) {
            return workspace;
        }

        workspace.setProvisioningStatus(WorkspaceProvisioningStatus.PROVISIONING);
        workspaceRepository.save(workspace);

        try {
            var systemRepository = systemRepositoryService.getPrimaryRepository();
            String baseBranch = systemRepository.getDefaultBranch();

            if (repositoryFactory != null) {
                DslGitRepository workspaceGit =
                        repositoryFactory.getWorkspaceRepository(workspace.getWorkspaceId());
                DslGitRepository systemGit = repositoryFactory.getSystemRepository();

                String systemDsl = systemGit.getDslAtHead(baseBranch);
                if (systemDsl != null) {
                    workspaceGit.commitDsl(
                            "main",
                            systemDsl,
                            username,
                            "Fork from shared/" + baseBranch);
                }

                workspace.setProvisioningStatus(WorkspaceProvisioningStatus.READY);
                workspace.setSourceRepositoryId(systemRepository.getRepositoryId());
                workspace.setTopologyMode(systemRepository.getTopologyMode());
                workspace.setBaseBranch(baseBranch);
                workspace.setBaseCommit(systemGit.getHeadCommit(baseBranch));
                workspace.setCurrentBranch("main");
                workspace.setCurrentCommit(workspaceGit.getHeadCommit("main"));
                workspace.setSyncTargetBranch(baseBranch);
                workspace.setProvisionedAt(Instant.now());
                workspace.setProvisioningError(null);
                workspaceRepository.save(workspace);

                log.info("Provisioned workspace for user '{}': repo='ws-{}', base='{}'",
                        username, workspace.getWorkspaceId(), baseBranch);
            } else {
                String userBranch = username + "/workspace/" + workspace.getWorkspaceId();
                String baseCommit = gitRepository.getHeadCommit(baseBranch);
                if (baseCommit != null) {
                    gitRepository.createBranch(userBranch, baseBranch);
                }

                workspace.setProvisioningStatus(WorkspaceProvisioningStatus.READY);
                workspace.setSourceRepositoryId(systemRepository.getRepositoryId());
                workspace.setTopologyMode(systemRepository.getTopologyMode());
                workspace.setBaseBranch(baseBranch);
                workspace.setBaseCommit(baseCommit);
                workspace.setCurrentBranch(userBranch);
                workspace.setCurrentCommit(baseCommit);
                workspace.setSyncTargetBranch(baseBranch);
                workspace.setProvisionedAt(Instant.now());
                workspace.setProvisioningError(null);
                workspaceRepository.save(workspace);

                log.info("Provisioned workspace for user '{}': branch='{}', base='{}'",
                        username, userBranch, baseBranch);
            }
            return workspace;
        } catch (Exception exception) {
            workspace.setProvisioningStatus(WorkspaceProvisioningStatus.FAILED);
            workspace.setProvisioningError(exception.getMessage());
            workspaceRepository.save(workspace);
            throw new RuntimeException(
                    "Could not provision workspace for " + username,
                    exception);
        }
    }

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

    private synchronized void ensurePersistentWorkspace(String username) {
        try {
            if (!workspaceRepository.existsByUsername(username)) {
                UserWorkspace workspace = new UserWorkspace();
                workspace.setWorkspaceId(UUID.randomUUID().toString());
                workspace.setUsername(username);
                workspace.setDisplayName(username + "'s workspace");
                workspace.setCurrentBranch("draft");
                workspace.setBaseBranch("draft");
                workspace.setShared(false);
                workspace.setDefault(true);
                workspace.setArchived(false);
                workspace.setProvisioningStatus(
                        WorkspaceProvisioningStatus.NOT_PROVISIONED);
                workspace.setTopologyMode(RepositoryTopologyMode.INTERNAL_SHARED);
                workspace.setCreatedAt(Instant.now());
                workspace.setLastAccessedAt(Instant.now());
                workspaceRepository.save(workspace);
                log.debug("Created persistent workspace for user '{}'", username);
            } else {
                workspaceRepository.findByUsernameAndSharedFalse(username)
                        .ifPresent(workspace -> {
                            workspace.setLastAccessedAt(Instant.now());
                            workspaceRepository.save(workspace);
                        });
            }
        } catch (Exception exception) {
            log.warn("Could not persist workspace for user '{}': {}",
                    username, exception.getMessage());
        }
    }

    private WorkspaceInfo toWorkspaceInfo(UserWorkspaceState state) {
        ContextRef context = state.getCurrentContext();
        String provisioningStatus = "READY";
        String topologyMode = "INTERNAL_SHARED";
        String sourceRepositoryId = null;
        String workspaceId = state.getUsername() + "-workspace";
        String displayName = state.getUsername() + "'s workspace";
        String description = null;
        boolean archived = false;
        boolean defaultWorkspace = false;

        try {
            UserWorkspace workspace = findActiveWorkspace(state.getUsername());
            if (workspace == null) {
                workspace = workspaceRepository
                        .findByUsernameAndSharedFalse(state.getUsername())
                        .orElse(null);
            }
            if (workspace != null) {
                provisioningStatus = workspace.getProvisioningStatus().name();
                topologyMode = workspace.getTopologyMode().name();
                sourceRepositoryId = workspace.getSourceRepositoryId();
                workspaceId = workspace.getWorkspaceId();
                displayName = workspace.getDisplayName();
                description = workspace.getDescription();
                archived = workspace.isArchived();
                defaultWorkspace = workspace.isDefault();
            }
        } catch (Exception exception) {
            log.debug("Could not read provisioning info for '{}': {}",
                    state.getUsername(), exception.getMessage());
        }

        return new WorkspaceInfo(
                workspaceId,
                state.getUsername(),
                displayName,
                context != null ? context.branch() : "draft",
                "draft",
                false,
                context,
                Instant.now(),
                Instant.now(),
                provisioningStatus,
                topologyMode,
                sourceRepositoryId,
                description,
                archived,
                defaultWorkspace);
    }

    /** Safely unwrap an Optional that may itself be null from an unstubbed mock. */
    private static <T> T safeOptional(java.util.Optional<T> optional) {
        return optional != null ? optional.orElse(null) : null;
    }
}