DeploymentContextPreparer.java

package com.cloudforge.core.config;

import com.cloudforge.core.enums.AuthMode;
import com.cloudforge.core.enums.NetworkMode;
import com.cloudforge.core.enums.RuntimeType;
import com.cloudforge.core.enums.SecurityProfile;
import com.cloudforge.core.enums.TopologyType;
import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.DatabaseSpec;

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

/**
 * Fills missing {@link DeploymentConfig} fields from ApplicationSpec, property files,
 * and auth/domain coherence rules — mirroring the interactive deployer when loading
 * a partial {@code deployment-context.json}.
 *
 * @since 3.3.0
 */
public final class DeploymentContextPreparer {

    private DeploymentContextPreparer() {
    }

    /**
     * Messages describing defaults applied or auth coercions performed.
     */
    public record PrepareResult(List<String> messages) {
        public PrepareResult() {
            this(new ArrayList<>());
        }

        void info(String message) {
            messages.add(message);
        }

        void warn(String message) {
            messages.add("⚠️  " + message);
        }
    }

    /**
     * Prepare configuration for synthesis or deployment.
     *
     * @param config deployment context (mutated in place)
     * @param spec application spec (falls back to {@code config.applicationSpec})
     * @param deploymentTarget {@code aws}, {@code ministack}, {@code localstack}, or {@code null}
     */
    public static PrepareResult prepare(
            DeploymentConfig config,
            ApplicationSpec spec,
            String deploymentTarget) {
        PrepareResult result = new PrepareResult();
        if (config == null) {
            return result;
        }

        ApplicationPropertyLoader.applyPropertyDefaults(config);

        ApplicationSpec appSpec = spec != null ? spec : config.applicationSpec;
        if (appSpec != null) {
            config.applicationSpec = appSpec;
            applyApplicationSpecFieldDefaults(config, appSpec, result);
            applyResourceDefaults(config, appSpec, result);
            applyDatabaseRequirements(config, appSpec, result);
        }

        applyStructuralDefaults(config, appSpec, result);
        applyOidcCoherence(config, appSpec, result);

        if (deploymentTarget != null && !deploymentTarget.isBlank() && appSpec != null) {
            coerceAuthModeForTarget(config, appSpec, deploymentTarget, result);
        }

        return result;
    }

    private static void applyApplicationSpecFieldDefaults(
            DeploymentConfig config,
            ApplicationSpec appSpec,
            PrepareResult result) {
        for (Field field : DeploymentConfig.class.getDeclaredFields()) {
            if (!field.isAnnotationPresent(com.cloudforge.core.annotation.ConfigField.class)) {
                continue;
            }
            ConfigFieldInfo info = ConfigFieldInfo.from(field);
            if (info.defaultFrom() == null || info.defaultFrom().isBlank()) {
                continue;
            }
            if (hasValue(info.getValue(config))) {
                continue;
            }
            Object resolved = DefaultValueResolver.resolve(info, appSpec, null);
            if (resolved != null) {
                info.setValue(config, resolved);
                result.info("Set " + info.fieldName() + " from ApplicationSpec (" + info.defaultFrom() + ")");
            }
        }
    }

    private static void applyResourceDefaults(
            DeploymentConfig config,
            ApplicationSpec appSpec,
            PrepareResult result) {
        RuntimeType runtime = config.runtime != null ? config.runtime : RuntimeType.FARGATE;
        if (runtime == RuntimeType.EC2) {
            if (config.instanceType == null || config.instanceType.isBlank()) {
                config.instanceType = appSpec.defaultInstanceType();
                result.info("Set instanceType from ApplicationSpec");
            }
            return;
        }
        if (config.cpu <= 0) {
            config.cpu = appSpec.defaultCpu();
            result.info("Set cpu from ApplicationSpec");
        }
        if (config.memory <= 0) {
            config.memory = appSpec.defaultMemory();
            result.info("Set memory from ApplicationSpec");
        }
    }

    private static void applyDatabaseRequirements(
            DeploymentConfig config,
            ApplicationSpec appSpec,
            PrepareResult result) {
        if (!(appSpec instanceof DatabaseSpec dbSpec)) {
            return;
        }
        DatabaseSpec.DatabaseRequirement requirement = dbSpec.databaseRequirement();
        if (requirement == null) {
            return;
        }
        if (requirement.type() == DatabaseSpec.DatabaseRequirement.RequirementType.REQUIRED
                && (config.provisionDatabase == null || !config.provisionDatabase)) {
            config.provisionDatabase = true;
            result.info("Enabled provisionDatabase (required by ApplicationSpec)");
        }
    }

    private static void applyStructuralDefaults(
            DeploymentConfig config, ApplicationSpec appSpec, PrepareResult result) {
        if (config.topology == null) {
            // Defaults to CMS_SERVICE for a CmsSpec implementer (WordPress, Drupal, etc.), not the
            // generic APPLICATION_SERVICE every other app falls back to — TopologyType.CMS_SERVICE
            // exists specifically for these ("auto-wires S3 media, Redis, CDN, RDS from CmsSpec" —
            // see its javadoc), so a flat default would leave a CMS app's actual deployed topology
            // mismatched with what the wizard shows/submits. Still just a default: topology stays
            // a genuinely selectable field (see DeploymentConfig.topology's allowedValues) so a
            // future CmsSpec-implementing integration can still override it either direction.
            config.topology = appSpec instanceof com.cloudforge.core.interfaces.CmsSpec
                ? TopologyType.CMS_SERVICE
                : TopologyType.APPLICATION_SERVICE;
            result.info("Set topology=" + config.topology);
        }
        if (config.networkMode == null) {
            config.networkMode = NetworkMode.PUBLIC;
            result.info("Set networkMode=public-no-nat");
        }
        if (config.securityProfile == null) {
            config.securityProfile = SecurityProfile.DEV;
            result.info("Set securityProfile=DEV");
        }
        if (config.runtime == null) {
            config.runtime = RuntimeType.FARGATE;
            result.info("Set runtime=FARGATE");
        }
        if (config.minInstanceCapacity <= 0) {
            config.minInstanceCapacity = 1;
        }
        if (config.maxInstanceCapacity <= 0) {
            config.maxInstanceCapacity = Math.max(1, config.minInstanceCapacity);
        }
        if (config.enableAutoScaling == null) {
            config.enableAutoScaling = config.maxInstanceCapacity > config.minInstanceCapacity;
        }
        if (config.wafEnabled == null) {
            config.wafEnabled = false;
        }
        if (config.enableMonitoring == null) {
            config.enableMonitoring = true;
        }
        if (config.enableEncryption == null) {
            config.enableEncryption = true;
        }
        if (config.awsConfigEnabled == null) {
            config.awsConfigEnabled = false;
        }
        if (config.guardDutyEnabled == null) {
            config.guardDutyEnabled = false;
        }
        if (config.auditManagerEnabled == null) {
            config.auditManagerEnabled = false;
        }
    }

    private static void applyOidcCoherence(
            DeploymentConfig config,
            ApplicationSpec appSpec,
            PrepareResult result) {
        AuthMode auth = config.authMode != null ? config.authMode : AuthMode.NONE;
        if (auth != AuthMode.ALB_OIDC && auth != AuthMode.APPLICATION_OIDC) {
            return;
        }
        if (appSpec != null && !appSpec.supportsOidcIntegration()) {
            result.warn("authMode " + auth + " requested but ApplicationSpec does not support OIDC");
            return;
        }

        if (config.enableSsl == null || !config.enableSsl) {
            config.enableSsl = true;
            result.info("Set enableSsl=true (OIDC requires HTTPS)");
        }

        if (config.oidcProvider == null || config.oidcProvider.isBlank() || "none".equals(config.oidcProvider)) {
            config.oidcProvider = "cognito";
            result.info("Set oidcProvider=cognito");
        }

        if ("cognito".equals(config.oidcProvider) || "cognito-saml".equals(config.oidcProvider)) {
            applyCognitoDefaults(config, result);
        }
    }

    private static void applyCognitoDefaults(DeploymentConfig config, PrepareResult result) {
        boolean usingExistingPool = config.cognitoUserPoolId != null && !config.cognitoUserPoolId.isBlank();
        if (!usingExistingPool && (config.cognitoAutoProvision == null || !config.cognitoAutoProvision)) {
            config.cognitoAutoProvision = true;
            result.info("Set cognitoAutoProvision=true");
        }
        if (config.cognitoAutoProvision == null || !config.cognitoAutoProvision) {
            return;
        }
        if (config.cognitoDomainPrefix == null || config.cognitoDomainPrefix.isBlank()) {
            String prefix = slug(config.stackName != null ? config.stackName : config.applicationId) + "-auth";
            config.cognitoDomainPrefix = prefix;
            result.info("Set cognitoDomainPrefix=" + prefix);
        }
        if (config.cognitoUserPoolName == null || config.cognitoUserPoolName.isBlank()) {
            config.cognitoUserPoolName = config.stackName + "-users";
        }
        if (config.cognitoCreateGroups == null) {
            config.cognitoCreateGroups = true;
            result.info("Set cognitoCreateGroups=true");
        }
        if (Boolean.TRUE.equals(config.cognitoCreateGroups)) {
            String app = config.applicationId != null ? config.applicationId : "app";
            if (config.cognitoAdminGroupName == null || config.cognitoAdminGroupName.isBlank()) {
                config.cognitoAdminGroupName = app + "-Admins";
            }
            if (config.cognitoUserGroupName == null || config.cognitoUserGroupName.isBlank()) {
                config.cognitoUserGroupName = app + "-Users";
            }
        }
        if (config.cognitoInitialAdminEmail == null || config.cognitoInitialAdminEmail.isBlank()) {
            String domain = config.domain != null && !config.domain.isBlank() ? config.domain : "local.test";
            config.cognitoInitialAdminEmail = "admin@" + domain;
            result.info("Set cognitoInitialAdminEmail=" + config.cognitoInitialAdminEmail);
        }
        if (config.cognitoMfaEnabled == null) {
            config.cognitoMfaEnabled = config.securityProfile == SecurityProfile.PRODUCTION;
        }
    }

    private static void coerceAuthModeForTarget(
            DeploymentConfig config,
            ApplicationSpec appSpec,
            String deploymentTarget,
            PrepareResult result) {
        List<String> allowed = appSpec.getSupportedAuthModes(deploymentTarget);
        if (allowed == null || allowed.isEmpty()) {
            return;
        }
        String current = config.authMode == null ? "none" : config.authMode.getValue();
        if (allowed.stream().anyMatch(mode -> mode.equalsIgnoreCase(current))) {
            return;
        }
        String coerced = appSpec.getRecommendedAuthMode(deploymentTarget);
        config.authMode = AuthMode.fromString(coerced);
        result.warn("authMode '" + current + "' is not supported for "
            + config.applicationId + " on " + deploymentTarget + "; using '" + coerced + "'");
    }

    private static boolean hasValue(Object value) {
        if (value == null) {
            return false;
        }
        if (value instanceof String s) {
            return !s.isBlank();
        }
        return true;
    }

    private static String slug(String value) {
        if (value == null || value.isBlank()) {
            return "cloudforge";
        }
        return value.trim()
            .toLowerCase(Locale.ROOT)
            .replaceAll("[^a-z0-9-]", "-")
            .replaceAll("-+", "-")
            .replaceAll("^-|-$", "");
    }
}