DefaultValueResolver.java

package com.cloudforge.core.config;

import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.FrameworkRules;

import java.lang.reflect.Method;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Resolves default values for configuration fields using layered priority.
 *
 * <p>Default value priority (highest to lowest):</p>
 * <ol>
 *   <li>User override in deployment-context.json (not handled here)</li>
 *   <li>FrameworkRules requirements (compliance-driven defaults)</li>
 *   <li>ApplicationSpec defaults (application-specific defaults via {@link ConfigFieldInfo#defaultFrom()})</li>
 *   <li>ConfigField annotation default (system-wide defaults)</li>
 * </ol>
 *
 * <h2>Convention-Based Lookup</h2>
 * <p>The {@code defaultFrom} attribute in {@link com.cloudforge.core.annotation.ConfigField}
 * specifies a method name or chain to call on the ApplicationSpec:</p>
 *
 * <pre>{@code
 * @ConfigField(
 *     displayName = "CPU Units",
 *     defaultFrom = "defaultCpu"  // Calls appSpec.defaultCpu()
 * )
 * public Integer cpu;
 *
 * @ConfigField(
 *     displayName = "Database Engine",
 *     defaultFrom = "databaseRequirement().engine"  // Chained: appSpec.databaseRequirement().engine()
 * )
 * public String databaseEngine;
 * }</pre>
 *
 * @since 3.0.0
 */
public class DefaultValueResolver {

    private static final Logger LOG = Logger.getLogger(DefaultValueResolver.class.getName());

    /**
     * Resolves the default value for a field using layered priority.
     *
     * @param fieldInfo field metadata
     * @param appSpec application spec for application-specific defaults
     * @param frameworks active compliance frameworks
     * @return resolved default value, or null if no default available
     */
    public static Object resolve(ConfigFieldInfo fieldInfo, ApplicationSpec appSpec, List<FrameworkRules<?>> frameworks) {
        // Layer 1: Try FrameworkRules defaults (highest priority after user)
        if (frameworks != null && !frameworks.isEmpty()) {
            for (FrameworkRules framework : frameworks) {
                Object frameworkDefault = resolveFromFramework(fieldInfo, framework);
                if (frameworkDefault != null) {
                    LOG.fine("Field '" + fieldInfo.fieldName() + "' default from " +
                        framework.getClass().getSimpleName() + ": " + frameworkDefault);
                    return frameworkDefault;
                }
            }
        }

        // Layer 2: Try ApplicationSpec defaults (via defaultFrom annotation)
        if (!fieldInfo.defaultFrom().isEmpty() && appSpec != null) {
            Object appSpecDefault = resolveFromApplicationSpec(fieldInfo, appSpec);
            if (appSpecDefault != null) {
                LOG.fine("Field '" + fieldInfo.fieldName() + "' default from ApplicationSpec: " + appSpecDefault);
                return appSpecDefault;
            }
        }

        // Layer 3: No custom default found - caller should use field's intrinsic default
        return null;
    }

    /**
     * Resolves default from ApplicationSpec using convention-based method lookup.
     *
     * <p>Supports simple method calls (e.g., "defaultCpu") and chained calls
     * (e.g., "databaseRequirement().engine").</p>
     */
    private static Object resolveFromApplicationSpec(ConfigFieldInfo fieldInfo, ApplicationSpec appSpec) {
        String expression = fieldInfo.defaultFrom();

        try {
            // Handle chained method calls: "databaseRequirement().engine"
            if (expression.contains(".")) {
                return resolveChainedMethod(appSpec, expression);
            }

            // Simple method call: "defaultCpu"
            Method method = appSpec.getClass().getMethod(expression);
            return method.invoke(appSpec);

        } catch (NoSuchMethodException e) {
            LOG.warning("Method '" + expression + "' not found on " + appSpec.getClass().getSimpleName());
            return null;
        } catch (Exception e) {
            LOG.log(Level.WARNING, "Failed to resolve default from ApplicationSpec: " + expression, e);
            return null;
        }
    }

    /**
     * Resolves chained method calls like "databaseRequirement().engine".
     */
    private static Object resolveChainedMethod(Object target, String expression) throws Exception {
        String[] parts = expression.split("\\.", 2);
        String methodName = parts[0].replace("()", "");

        // Invoke first method
        Method method = target.getClass().getMethod(methodName);
        Object result = method.invoke(target);

        if (result == null) {
            return null;
        }

        // If there are more parts, continue the chain
        if (parts.length > 1) {
            return resolveChainedMethod(result, parts[1]);
        }

        return result;
    }

    /**
     * Resolves default from FrameworkRules.
     *
     * <p>This is a placeholder for future implementation. FrameworkRules will provide
     * a method like {@code getDefaultForField(String fieldName)} that returns compliance-driven
     * defaults.</p>
     */
    private static Object resolveFromFramework(ConfigFieldInfo fieldInfo, FrameworkRules<?> framework) {
        // TODO: Implement FrameworkRules.getDefaultForField() in Phase 2
        // For now, frameworks can only apply defaults via applyDefaults(DeploymentConfig)
        return null;
    }

    /**
     * Resolves default with fallback to field's annotated default.
     *
     * <p>Convenience method that returns the field's current value as fallback.</p>
     *
     * @param fieldInfo field metadata
     * @param appSpec application spec
     * @param frameworks active frameworks
     * @param config current config (for reading current field value as fallback)
     * @return resolved default value, or current field value if no default available
     */
    public static Object resolveWithFallback(ConfigFieldInfo fieldInfo, ApplicationSpec appSpec,
                                            List<FrameworkRules<?>> frameworks, Object config) {
        Object resolved = resolve(fieldInfo, appSpec, frameworks);
        if (resolved != null) {
            return resolved;
        }

        // Fallback to current field value
        return fieldInfo.getValue(config);
    }
}