DslValidator.java

package com.taxonomy.dsl.validation;

import com.taxonomy.dsl.model.*;

import java.util.*;

/**
 * Validates a {@link CanonicalArchitectureModel} for structural and semantic correctness.
 *
 * <p>Checks include:
 * <ul>
 *   <li>Duplicate element/requirement/view/evidence IDs</li>
 *   <li>Relations referencing unknown element IDs</li>
 *   <li>Mappings referencing unknown requirement or element IDs</li>
 *   <li>Invalid relation types against the type matrix</li>
 *   <li>Invalid type combinations for relation source/target</li>
 *   <li>Invalid status values on relations</li>
 *   <li>Missing required fields</li>
 * </ul>
 */
public class DslValidator {

    /** Recognized relation types from the type matrix. */
    private static final Set<String> VALID_RELATION_TYPES = Set.of(
            "REALIZES", "SUPPORTS", "CONSUMES", "USES", "FULFILLS",
            "ASSIGNED_TO", "DEPENDS_ON", "PRODUCES", "COMMUNICATES_WITH",
            "CONTAINS", "REQUIRES", "RELATED_TO");

    /** Recognized relation statuses. */
    private static final Set<String> VALID_STATUSES = Set.of(
            "proposed", "provisional", "accepted", "rejected");

    /**
     * Type compatibility matrix: relation type → allowed (sourceRoot → targetRoots).
     * Mirrors {@code RelationCompatibilityMatrix} but usable without Spring.
     */
    private static final Map<String, Map<String, Set<String>>> TYPE_MATRIX;

    static {
        Map<String, Map<String, Set<String>>> m = new LinkedHashMap<>();
        m.put("REALIZES",          Map.of("CP", Set.of("CR")));
        m.put("SUPPORTS",          Map.of("CR", Set.of("BP", "BR"),
                                          "CI", Set.of("BP", "BR"),
                                          "CO", Set.of("BP"),
                                          "UA", Set.of("BP")));
        m.put("CONSUMES",          Map.of("BP", Set.of("IP"),
                                          "CR", Set.of("IP"),
                                          "CI", Set.of("IP")));
        m.put("USES",              Map.of("UA", Set.of("CR", "CI", "CO"),
                                          "SY", Set.of("SY", "CR"),
                                          "BP", Set.of("CR", "CI"),
                                          "BR", Set.of("IP")));
        m.put("FULFILLS",          Map.of("CI", Set.of("CP"), "CR", Set.of("CP")));
        m.put("ASSIGNED_TO",       Map.of("BR", Set.of("BP")));
        m.put("DEPENDS_ON",        Map.of("CR", Set.of("CR", "CI"),
                                          "CI", Set.of("CR"),
                                          "CO", Set.of("CR"),
                                          "SY", Set.of("SY"),
                                          "CM", Set.of("CM", "CR")));
        m.put("PRODUCES",          Map.of("BP", Set.of("IP"),
                                          "CR", Set.of("IP"),
                                          "CI", Set.of("IP"),
                                          "UA", Set.of("IP")));
        m.put("COMMUNICATES_WITH", Map.of("CO", Set.of("CR", "CI", "UA")));
        m.put("CONTAINS",          Map.of("SY", Set.of("UA", "CM"), "CM", Set.of("CM")));
        m.put("REQUIRES",          Map.of("CP", Set.of("IP")));
        // RELATED_TO has no restrictions
        TYPE_MATRIX = Collections.unmodifiableMap(m);
    }

    /** Immutable metamodel rules shared by strict semantic command validation and UI schemas. */
    public static Map<String, Map<String, Set<String>>> relationTypeRules() {
        return TYPE_MATRIX;
    }

    public static Set<String> relationTypes() {
        return VALID_RELATION_TYPES;
    }

    public static Set<String> relationStatuses() {
        return VALID_STATUSES;
    }

    /**
     * Validate a canonical architecture model.
     */
    public DslValidationResult validate(CanonicalArchitectureModel model) {
        DslValidationResult result = new DslValidationResult();

        Set<String> elementIds = checkDuplicateIds(model.getElements(), "element", result);
        Set<String> requirementIds = checkDuplicateRequirementIds(model.getRequirements(), result);
        checkDuplicateViewIds(model.getViews(), result);
        checkDuplicateEvidenceIds(model.getEvidence(), result);

        // Combine all known IDs for reference resolution
        Set<String> allIds = new HashSet<>(elementIds);
        allIds.addAll(requirementIds);

        // Build element type lookup for type-combination validation
        Map<String, String> elementTypeMap = new HashMap<>();
        for (ArchitectureElement el : model.getElements()) {
            if (el.getId() != null && el.getType() != null) {
                elementTypeMap.put(el.getId(), el.getType());
            }
        }

        validateRelations(model.getRelations(), allIds, elementTypeMap, result);
        validateMappings(model.getMappings(), requirementIds, elementIds, result);
        validateElements(model.getElements(), result);

        return result;
    }

    private Set<String> checkDuplicateIds(List<ArchitectureElement> elements, String kind,
                                          DslValidationResult result) {
        Set<String> seen = new LinkedHashSet<>();
        for (ArchitectureElement el : elements) {
            if (el.getId() == null || el.getId().isBlank()) {
                result.addError("Element is missing an ID");
            } else if (!seen.add(el.getId())) {
                result.addError("Duplicate " + kind + " ID: " + el.getId());
            }
        }
        return seen;
    }

    private Set<String> checkDuplicateRequirementIds(List<ArchitectureRequirement> requirements,
                                                     DslValidationResult result) {
        Set<String> seen = new LinkedHashSet<>();
        for (ArchitectureRequirement req : requirements) {
            if (req.getId() == null || req.getId().isBlank()) {
                result.addError("Requirement is missing an ID");
            } else if (!seen.add(req.getId())) {
                result.addError("Duplicate requirement ID: " + req.getId());
            }
        }
        return seen;
    }

    private void checkDuplicateViewIds(List<ArchitectureView> views, DslValidationResult result) {
        Set<String> seen = new HashSet<>();
        for (ArchitectureView view : views) {
            if (view.getId() == null || view.getId().isBlank()) {
                result.addError("View is missing an ID");
            } else if (!seen.add(view.getId())) {
                result.addError("Duplicate view ID: " + view.getId());
            }
        }
    }

    private void checkDuplicateEvidenceIds(List<ArchitectureEvidence> evidence, DslValidationResult result) {
        Set<String> seen = new HashSet<>();
        for (ArchitectureEvidence ev : evidence) {
            if (ev.getId() == null || ev.getId().isBlank()) {
                result.addError("Evidence is missing an ID");
            } else if (!seen.add(ev.getId())) {
                result.addError("Duplicate evidence ID: " + ev.getId());
            }
        }
    }

    private void validateRelations(List<ArchitectureRelation> relations, Set<String> allIds,
                                   Map<String, String> elementTypeMap,
                                   DslValidationResult result) {
        for (ArchitectureRelation rel : relations) {
            if (rel.getSourceId() == null || rel.getSourceId().isBlank()) {
                result.addError("Relation is missing a source ID");
            } else if (!allIds.contains(rel.getSourceId())) {
                result.addWarning("Relation references unknown source ID: " + rel.getSourceId());
            }

            if (rel.getTargetId() == null || rel.getTargetId().isBlank()) {
                result.addError("Relation is missing a target ID");
            } else if (!allIds.contains(rel.getTargetId())) {
                result.addWarning("Relation references unknown target ID: " + rel.getTargetId());
            }

            if (rel.getRelationType() != null && !VALID_RELATION_TYPES.contains(rel.getRelationType())) {
                result.addWarning("Unknown relation type: " + rel.getRelationType());
            }

            if (rel.getStatus() != null && !VALID_STATUSES.contains(rel.getStatus())) {
                result.addError("Invalid relation status: " + rel.getStatus()
                        + " (valid: " + VALID_STATUSES + ")");
            }

            // Type-combination validation using the type matrix
            if (rel.getRelationType() != null && rel.getSourceId() != null && rel.getTargetId() != null) {
                validateTypeCombination(rel, elementTypeMap, result);
            }
        }
    }

    /**
     * Check that the source and target element types are compatible with the relation type,
     * using the type matrix (e.g., REALIZES requires CP→CR).
     */
    private void validateTypeCombination(ArchitectureRelation rel,
                                         Map<String, String> elementTypeMap,
                                         DslValidationResult result) {
        Map<String, Set<String>> rules = TYPE_MATRIX.get(rel.getRelationType());
        if (rules == null) {
            return; // No type matrix rule (e.g., RELATED_TO or unknown type)
        }

        String sourceRoot = resolveTaxonomyRootCode(rel.getSourceId(), elementTypeMap);
        String targetRoot = resolveTaxonomyRootCode(rel.getTargetId(), elementTypeMap);

        if (sourceRoot == null || targetRoot == null) {
            return; // Can't determine type — skip validation
        }

        Set<String> allowedTargets = rules.get(sourceRoot);
        if (allowedTargets == null) {
            result.addWarning("Relation " + rel.getRelationType() + ": source "
                    + rel.getSourceId() + " (root " + sourceRoot
                    + ") is not a valid source type for " + rel.getRelationType());
        } else if (!allowedTargets.contains(targetRoot)) {
            result.addWarning("Relation " + rel.getRelationType() + ": target "
                    + rel.getTargetId() + " (root " + targetRoot
                    + ") is not a valid target type; expected one of " + allowedTargets);
        }
    }

    /**
     * Resolve the taxonomy root code for an element ID, using either the
     * element type map (from DSL) or the ID prefix (e.g., CP-1023 → CP).
     */
    private String resolveTaxonomyRootCode(String elementId, Map<String, String> elementTypeMap) {
        // Try element type from DSL model first
        String typeName = elementTypeMap.get(elementId);
        if (typeName != null) {
            String root = TaxonomyRootTypes.rootFor(typeName);
            if (root != null) return root;
        }
        // Fall back to ID prefix
        return TaxonomyRootTypes.rootFromId(elementId);
    }

    private void validateMappings(List<RequirementMapping> mappings,
                                  Set<String> requirementIds,
                                  Set<String> elementIds,
                                  DslValidationResult result) {
        for (RequirementMapping m : mappings) {
            if (m.getRequirementId() == null || m.getRequirementId().isBlank()) {
                result.addError("Mapping is missing a requirement ID");
            } else if (!requirementIds.contains(m.getRequirementId())) {
                result.addWarning("Mapping references unknown requirement ID: " + m.getRequirementId());
            }

            if (m.getElementId() == null || m.getElementId().isBlank()) {
                result.addError("Mapping is missing an element ID");
            } else if (!elementIds.contains(m.getElementId())) {
                result.addWarning("Mapping references unknown element ID: " + m.getElementId());
            }
        }
    }

    private void validateElements(List<ArchitectureElement> elements, DslValidationResult result) {
        for (ArchitectureElement el : elements) {
            if (el.getTitle() == null || el.getTitle().isBlank()) {
                result.addWarning("Element " + el.getId() + " is missing a title");
            }
        }
    }
}