ContextHistoryService.java

package com.taxonomy.versioning.service;

import com.taxonomy.versioning.model.ContextHistoryRecord;
import com.taxonomy.versioning.repository.ContextHistoryRecordRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.List;

/**
 * Manages persistent context navigation history with origin tracking.
 *
 * <p>Each navigation event captures the source and destination contexts,
 * branches, and commits, together with the reason for the navigation
 * (e.g. {@code "COMPARE"}, {@code "SEARCH_OPEN"}, {@code "MANUAL_SWITCH"}).
 *
 * <p>The {@code originContextId} field supports "return-to-origin"
 * navigation: when a user drills into history or a comparison, the
 * original context is preserved so they can jump back to where they
 * started.
 *
 * <p>History is capped at 50 entries per user to prevent unbounded growth.
 */
@Service
public class ContextHistoryService {

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

    private final ContextHistoryRecordRepository historyRepository;

    public ContextHistoryService(ContextHistoryRecordRepository historyRepository) {
        this.historyRepository = historyRepository;
    }

    /**
     * Record a navigation event in the user's persistent history.
     *
     * @param username        the authenticated user's username
     * @param fromContextId   the context the user navigated away from (may be null)
     * @param toContextId     the context the user navigated to
     * @param fromBranch      the branch the user was on before navigation (may be null)
     * @param toBranch        the branch the user navigated to
     * @param fromCommitId    the commit SHA before navigation (may be null)
     * @param toCommitId      the commit SHA after navigation (may be null)
     * @param reason          the reason for navigation (e.g. "COMPARE", "SEARCH_OPEN")
     * @param originContextId the original context for return-to-origin support (may be null)
     */
    public void recordNavigation(String username,
                                 String fromContextId,
                                 String toContextId,
                                 String fromBranch,
                                 String toBranch,
                                 String fromCommitId,
                                 String toCommitId,
                                 String reason,
                                 String originContextId) {
        try {
            ContextHistoryRecord record = new ContextHistoryRecord();
            record.setUsername(username);
            record.setFromContextId(fromContextId);
            record.setToContextId(toContextId);
            record.setFromBranch(fromBranch);
            record.setToBranch(toBranch);
            record.setFromCommitId(fromCommitId);
            record.setToCommitId(toCommitId);
            record.setReason(reason);
            record.setOriginContextId(originContextId);
            record.setCreatedAt(Instant.now());

            historyRepository.save(record);
            log.debug("User '{}': recorded navigation {} -> {} (reason: {})",
                    username, fromContextId, toContextId, reason);

            // Enforce 50-entry cap: delete oldest entries beyond the limit
            trimHistory(username);
        } catch (Exception e) {
            // Non-fatal: navigation history is informational
            log.warn("Could not record navigation for user '{}': {}",
                    username, e.getMessage());
        }
    }

    private static final int MAX_HISTORY_ENTRIES = 50;

    private void trimHistory(String username) {
        try {
            long count = historyRepository.countByUsername(username);
            if (count <= MAX_HISTORY_ENTRIES) {
                return; // Nothing to trim
            }
            // Only load old entries when trimming is needed
            List<ContextHistoryRecord> all =
                    historyRepository.findByUsernameOrderByCreatedAtDesc(username);
            List<ContextHistoryRecord> toDelete =
                    all.subList(MAX_HISTORY_ENTRIES, all.size());
            historyRepository.deleteAll(toDelete);
            log.debug("User '{}': trimmed {} old history entries",
                    username, toDelete.size());
        } catch (Exception e) {
            log.warn("Could not trim history for user '{}': {}",
                    username, e.getMessage());
        }
    }

    /**
     * Get the user's recent navigation history (max 50 entries).
     *
     * <p>Results are ordered by creation time, newest first.
     *
     * @param username the authenticated user's username
     * @return list of history records, newest first (never null)
     */
    public List<ContextHistoryRecord> getHistory(String username) {
        try {
            return historyRepository.findTop50ByUsernameOrderByCreatedAtDesc(username);
        } catch (Exception e) {
            log.warn("Could not retrieve history for user '{}': {}", username, e.getMessage());
            return List.of();
        }
    }

    /**
     * Delete all navigation history for a user.
     *
     * <p>This is a destructive operation. It removes all persistent history
     * records for the specified user.
     *
     * @param username the user whose history to clear
     */
    public void clearHistory(String username) {
        try {
            historyRepository.deleteByUsername(username);
            log.info("Cleared navigation history for user '{}'", username);
        } catch (Exception e) {
            log.warn("Could not clear history for user '{}': {}", username, e.getMessage());
        }
    }
}