ConfigurationIntrospector.java

package com.cloudforge.core.config;

import com.cloudforge.core.annotation.ConfigField;
import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.DatabaseSpec;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * Discovers and filters configuration fields using reflection and annotations.
 *
 * <p>This is the core of the Configuration Introspection system, providing automatic
 * field discovery based on {@link ConfigField} annotations and application capabilities.</p>
 *
 * <h2>Usage</h2>
 * <pre>{@code
 * // Discover all fields for an application
 * List<ConfigFieldInfo> fields = ConfigurationIntrospector.discoverFields(gitlabSpec);
 *
 * // Discover fields in a specific category
 * List<ConfigFieldInfo> databaseFields = ConfigurationIntrospector.discoverFields(gitlabSpec, "database");
 *
 * // Filter by visibility
 * List<ConfigFieldInfo> visibleFields = fields.stream()
 *     .filter(f -> f.isVisible(config))
 *     .toList();
 * }</pre>
 *
 * @since 3.0.0
 */
public class ConfigurationIntrospector {

    /**
     * Discovers all configuration fields from DeploymentConfig.
     *
     * <p>Returns fields sorted by category order, then field order, then display name.</p>
     *
     * @param appSpec application spec for determining field visibility
     * @return list of all discovered fields
     */
    public static List<ConfigFieldInfo> discoverFields(ApplicationSpec appSpec) {
        return discoverFields(appSpec, null);
    }

    /**
     * The values a form or prompt should offer for a field. Most fields offer their own
     * {@code allowedValues}. The database fields offer what the selected application supports:
     * {@code databaseEngine} lists its engines, and {@code databaseVersion} lists the versions of
     * the engine chosen in {@code config}, or of the application's own engine when none is chosen.
     * The Manager and the CLI both read this, so an application's {@link DatabaseSpec#supportedDatabases()}
     * shows up in each without further changes.
     *
     * @param field the field being offered
     * @param appSpec the selected application, may be null
     * @param config the configuration chosen so far, may be null
     * @return the offered values, empty when the field accepts free input
     */
    public static String[] allowedValues(ConfigFieldInfo field, ApplicationSpec appSpec, Object config) {
        boolean engine = "databaseEngine".equals(field.fieldName());
        boolean version = "databaseVersion".equals(field.fieldName());
        if ((engine || version) && appSpec instanceof DatabaseSpec databaseSpec
                && databaseSpec.databaseRequirement() != null
                && databaseSpec.databaseRequirement().type() != DatabaseSpec.DatabaseRequirement.RequirementType.NONE) {
            List<DatabaseSpec.DatabaseOption> supported = databaseSpec.supportedDatabases();
            if (engine) {
                return supported.stream().map(DatabaseSpec.DatabaseOption::engine).distinct().toArray(String[]::new);
            }
            String chosen = config instanceof DeploymentConfig deployment && deployment.databaseEngine != null
                ? deployment.databaseEngine
                : databaseSpec.databaseRequirement().engine();
            return supported.stream()
                .filter(option -> option.engine().equalsIgnoreCase(chosen))
                .map(DatabaseSpec.DatabaseOption::version)
                .toArray(String[]::new);
        }
        return field.allowedValues();
    }

    /**
     * Discovers configuration fields for a specific category.
     *
     * <p>Returns fields sorted by field order, then display name.</p>
     *
     * @param appSpec application spec for determining field visibility
     * @param category category to filter by, or null for all categories
     * @return list of discovered fields matching the category
     */
    // codeql[java/unused-parameter] -- appSpec is unused here: per-appSpec visibility filtering
    // happens in the sibling discoverVisibleFields(appSpec, config) instead, which calls this
    // method first; appSpec is kept in this signature for symmetry with that sibling.
    public static List<ConfigFieldInfo> discoverFields(ApplicationSpec appSpec, String category) {
        List<ConfigFieldInfo> fields = new ArrayList<>();

        // Use reflection to find all fields in DeploymentConfig
        // For now, we'll scan the DeploymentConfig class
        Class<?> configClass = getDeploymentConfigClass();
        if (configClass == null) {
            return fields;
        }

        for (Field field : configClass.getDeclaredFields()) {
            if (!field.isAnnotationPresent(ConfigField.class)) {
                continue;
            }

            ConfigFieldInfo fieldInfo = ConfigFieldInfo.from(field);

            // Filter by category if specified
            if (category != null && !category.isEmpty() && !fieldInfo.category().equals(category)) {
                continue;
            }

            fields.add(fieldInfo);
        }

        // Sort by category order (if applicable), field order, then display name
        fields.sort(Comparator
            .comparing((ConfigFieldInfo f) -> getCategoryOrder(f.category()))
            .thenComparing(ConfigFieldInfo::order)
            .thenComparing(ConfigFieldInfo::displayName));

        return fields;
    }

    /**
     * Discovers visible fields only (based on application capabilities and configuration state).
     *
     * @param appSpec application spec for determining field visibility
     * @param config current configuration state for evaluating visibility expressions
     * @return list of visible fields
     */
    public static List<ConfigFieldInfo> discoverVisibleFields(ApplicationSpec appSpec, Object config) {
        return discoverFields(appSpec).stream()
            .filter(field -> field.isVisible(appSpec, config))
            .toList();
    }

    /**
     * Discovers fields by category with visibility filtering.
     *
     * @param appSpec application spec
     * @param config current configuration state
     * @param category category to filter by
     * @return list of visible fields in the category
     */
    public static List<ConfigFieldInfo> discoverVisibleFields(ApplicationSpec appSpec, Object config, String category) {
        return discoverFields(appSpec, category).stream()
            .filter(field -> field.isVisible(appSpec, config))
            .toList();
    }

    /**
     * Gets the DeploymentConfig class using reflection.
     *
     * <p>Tries multiple potential package locations for DeploymentConfig.</p>
     */
    private static Class<?> getDeploymentConfigClass() {
        // Try common package locations
        String[] packagePrefixes = {
            "com.cloudforge.core.config.",
            "com.cloudforgeci.api.core."
        };

        for (String prefix : packagePrefixes) {
            try {
                return Class.forName(prefix + "DeploymentConfig");
            } catch (ClassNotFoundException e) {
                // Try next package
            }
        }

        return null;
    }

    /**
     * Gets the display order for a category.
     *
     * <p>Categories are displayed in this order:</p>
     * <ol>
     *   <li>basic - Essential configuration</li>
     *   <li>network - Domain, SSL, networking</li>
     *   <li>security - Authentication, encryption, general hardening</li>
     *   <li>database - RDS database provisioning</li>
     *   <li>resources - CPU, memory, scaling, instance sizing</li>
     *   <li>monitoring - CloudWatch, generic ops alerting</li>
     *   <li>compliance - AWS Config, GuardDuty, Security Hub, framework selection, and
     *       every other compliance-specific setting</li>
     * </ol>
     */
    private static int getCategoryOrder(String category) {
        return switch (category) {
            case "basic" -> 1;
            // Domain settings are a distinct UI group, but must remain beside
            // network configuration rather than falling into the unknown tail.
            case "domain" -> 2;
            case "network" -> 3;
            case "security" -> 4;
            case "database" -> 5;
            case "resources" -> 6;
            case "monitoring" -> 7;
            case "compliance" -> 8;
            default -> 1000;  // Unknown categories go last
        };
    }
}