TaxonomyService.java

package fr.tiogars.chat.domains.taxonomy;

import java.util.HashSet;
import java.util.List;

import fr.tiogars.chat.domains.tag.entities.TagEntity;
import fr.tiogars.chat.domains.tag.models.CreateTagRequest;
import fr.tiogars.chat.domains.tag.models.UpdateTagRequest;
import fr.tiogars.chat.domains.tag.models.Tag;
import fr.tiogars.chat.domains.tag.repositories.TagRepository;
import fr.tiogars.chat.domains.nomenclature.entities.NomenclatureEntity;
import fr.tiogars.chat.domains.nomenclature.models.CreateNomenclatureRequest;
import fr.tiogars.chat.domains.nomenclature.models.UpdateNomenclatureRequest;
import fr.tiogars.chat.domains.nomenclature.models.Nomenclature;
import fr.tiogars.chat.domains.nomenclature.repositories.NomenclatureRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

@Service
@Transactional
public class TaxonomyService {
    private final TagRepository tags;
    private final NomenclatureRepository nomenclatures;

    public TaxonomyService(TagRepository tags, NomenclatureRepository nomenclatures) {
        this.tags = tags;
        this.nomenclatures = nomenclatures;
    }

    public List<Tag> tags() {
        return tags.findAllByOrderByCodeAsc().stream().map(Tag::from).toList();
    }

    public Tag tag(Long id) {
        return Tag.from(tags.findById(id).orElseThrow(() -> missing("Tag")));
    }

    public Tag createTag(CreateTagRequest request) {
        if (tags.existsByCode(request.code())) {
            throw conflict("Tag code already exists.");
        }
        return Tag.from(tags.saveAndFlush(new TagEntity(request.code(), request.label(), request.description())));
    }

    public Tag updateTag(Long id, UpdateTagRequest request) {
        TagEntity entity = tags.lockById(id).orElseThrow(() -> missing("Tag"));
        entity.update(request.label(), request.description());
        return Tag.from(tags.saveAndFlush(entity));
    }

    public void deleteTag(Long id) {
        TagEntity entity = tags.lockById(id).orElseThrow(() -> missing("Tag"));
        if (tags.countTaskReferences(id) > 0) {
            throw conflict("Tag is referenced by a task.");
        }
        tags.delete(entity);
        tags.flush();
    }

    public List<Nomenclature> nomenclatures() {
        return nomenclatures.findAllByOrderByCodeAsc().stream().map(Nomenclature::from).toList();
    }

    public Nomenclature nomenclature(Long id) {
        return Nomenclature.from(nomenclatures.findById(id).orElseThrow(() -> missing("Nomenclature")));
    }

    public Nomenclature createNomenclature(CreateNomenclatureRequest request) {
        nomenclatures.lockForest();
        if (nomenclatures.existsByCode(request.code())) {
            throw conflict("Nomenclature code already exists.");
        }
        NomenclatureEntity entity = new NomenclatureEntity(request.code(), request.label(), request.description());
        entity.setParent(parent(null, request.parentId()));
        return Nomenclature.from(nomenclatures.saveAndFlush(entity));
    }

    public Nomenclature updateNomenclature(Long id, UpdateNomenclatureRequest request) {
        nomenclatures.lockForest();
        NomenclatureEntity entity = nomenclatures.findById(id).orElseThrow(() -> missing("Nomenclature"));
        entity.setParent(parent(id, request.parentId()));
        entity.update(request.label(), request.description());
        return Nomenclature.from(nomenclatures.saveAndFlush(entity));
    }

    public void deleteNomenclature(Long id) {
        nomenclatures.lockForest();
        NomenclatureEntity entity = nomenclatures.findById(id).orElseThrow(() -> missing("Nomenclature"));
        if (nomenclatures.existsByParentId(id) || nomenclatures.countTaskReferences(id) > 0) {
            throw conflict("Nomenclature is referenced by a task or child.");
        }
        nomenclatures.delete(entity);
        nomenclatures.flush();
    }

    private NomenclatureEntity parent(Long id, Long parentId) {
        if (parentId == null) {
            return null;
        }
        NomenclatureEntity parent = nomenclatures.findById(parentId)
                .orElseThrow(() -> invalid("Parent nomenclature does not exist."));
        HashSet<Long> visited = new HashSet<>();
        for (NomenclatureEntity ancestor = parent; ancestor != null; ancestor = ancestor.getParent()) {
            if (ancestor.getId().equals(id) || !visited.add(ancestor.getId())) {
                throw invalid("Nomenclature parent would create a cycle.");
            }
        }
        return parent;
    }

    private static ResponseStatusException missing(String type) {
        return new ResponseStatusException(HttpStatus.NOT_FOUND, type + " not found.");
    }

    private static ResponseStatusException invalid(String message) {
        return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
    }

    private static ResponseStatusException conflict(String message) {
        return new ResponseStatusException(HttpStatus.CONFLICT, message);
    }
}