DeploymentConfig.java

package com.cloudforge.core.config;

import com.cloudforge.core.annotation.ConfigField;
import com.cloudforge.core.annotation.FieldTag;
import com.cloudforge.core.enums.AuthMode;
import com.cloudforge.core.enums.ComplianceFrameworkType;
import com.cloudforge.core.enums.ComplianceMode;
import com.cloudforge.core.enums.LoadBalancerType;
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.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Universal deployment configuration for CloudForge applications.
 *
 * <p>This is the canonical configuration structure used by both interactive deployers
 * and non-interactive deployment tools. It maps directly to deployment-context.json
 * and can be serialized/deserialized via Jackson.</p>
 *
 * <p><b>Architecture:</b> This class lives in cloudforge-core (the contract layer) as
 * it defines the data model interface between libraries and consumers. This ensures
 * cloudforge-cli and other consumers always use the latest configuration schema without
 * duplication.</p>
 *
 * @since CloudForge 3.0.0
 */
public class DeploymentConfig {

    // ========== Basic Configuration ==========

    /** CloudFormation stack name */
    @ConfigField(
        displayName = "Stack Name",
        description = "CloudFormation stack name (lowercase alphanumeric with hyphens)",
        category = "basic",
        required = true,
        pattern = "^[a-zA-Z][a-zA-Z0-9-]{0,127}$",
        example = "my-app-stack",
        order = 10
    )
    public String stackName;

    /** Environment name (e.g., "dev", "staging", "production") */
    @ConfigField(
        displayName = "Environment",
        description = "Deployment environment name",
        category = "basic",
        allowedValues = {"dev", "staging", "prod"},
        example = "dev",
        order = 20
    )
    public String environment;

    /** Application identifier (e.g., "jenkins", "gitlab", "vault") */
    @ConfigField(
        displayName = "Application ID",
        description = "Application identifier from available plugins",
        category = "basic",
        required = true,
        order = 5
    )
    public String applicationId;

    /** Human-readable application name */
    @ConfigField(
        displayName = "Application Name",
        description = "Human-readable application display name",
        category = "basic",
        order = 6
    )
    public String applicationName;

    /** ApplicationSpec instance (not serialized to JSON) */
    @JsonIgnore
    public ApplicationSpec applicationSpec;

    // ========== Domain Configuration ==========

    /**
     * Subdomain prefix (e.g., "ci", "gitlab") — ordered ahead of {@link #domain}: you pick what
     * this app is called (the subdomain) before which domain it hangs off of, and the combined
     * result (subdomain + "." + domain) is what gets requested, not the other way
     * around.
     */
    @ConfigField(
        displayName = "Subdomain",
        description = "Subdomain prefix (e.g., 'ci' for ci.example.com)",
        category = "domain",
        pattern = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$",
        example = "ci",
        order = 10
    )
    public String subdomain;

    /** Primary domain (e.g., "example.com") */
    @ConfigField(
        displayName = "Domain",
        description = "Primary domain for the application (e.g., example.com)",
        category = "domain",
        pattern = "^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$",
        example = "example.com",
        order = 20
    )
    public String domain;

    /**
     * Fully qualified domain name — always {@code subdomain + "." + domain} once both are set;
     * this field only matters when you need to override that computed result directly (e.g. a
     * domain structure the subdomain+domain pair can't express), or when a caller only has the
     * full FQDN available and no reliable way to split it (e.g. recovering it from a live
     * CloudFormation template's ACM certificate, where the domain/subdomain boundary isn't
     * knowable without a hosted-zone lookup). Previously {@code @JsonIgnore}d on the theory that
     * it should never round-trip as its own JSON key — that silently dropped any caller-supplied
     * override during deserialization (an explicit {@code fqdn} in a submitted JSON body was
     * discarded, reverting to null and falling through to the subdomain+domain computation
     * instead), which is the opposite of "override."
     */
    @ConfigField(
        displayName = "FQDN (advanced override)",
        description = "Computed as Subdomain + \".\" + Domain — only set this directly to override that computed value",
        category = "domain",
        order = 30
    )
    public String fqdn;

    /** Enable SSL certificate via ACM */
    @ConfigField(
        displayName = "Enable SSL",
        description = "Enable SSL/TLS with AWS Certificate Manager",
        category = "domain",
        order = 30
    )
    public Boolean enableSsl;

    /**
     * ARN of an existing ACM certificate to use for the ALB's HTTPS listener, instead of
     * provisioning a new one. Takes priority over both of {@code FargateRuntimeConfiguration}'s
     * other two certificate paths — the DNS-validated public cert (needs a Route53 hosted zone
     * this deployment controls) and the AWS Private CA cert (issued for the bare ALB DNS name,
     * NOT trusted by browsers — see that class's comments). This is how a deployment gets a
     * publicly-trusted certificate without either of those: import your own cert
     * (issued by any public CA — ACM's own DNS/email validation, Let's Encrypt, a purchased
     * cert, ...) into ACM yourself first (e.g. {@code aws acm import-certificate}, entirely
     * within your own account — the key material never has to pass through this deployment
     * config or CloudFormation), then point this at the resulting ARN.
     */
    @ConfigField(
        displayName = "Existing Certificate ARN",
        description = "ARN of an ACM certificate you've already issued/imported — used as-is instead of provisioning a new one",
        category = "domain",
        pattern = "^arn:aws[a-zA-Z-]*:acm:[a-z0-9-]+:\\d{12}:certificate/[a-f0-9-]+$",
        example = "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
        visibleWhen = "enableSsl == true",
        order = 35
    )
    public String certificateArn;

    // ========== Runtime Configuration ==========

    /** Runtime type (FARGATE or EC2) */
    @ConfigField(
        displayName = "Runtime Type",
        description = "Container runtime: FARGATE (serverless) or EC2 (instance-based)",
        category = "basic",
        allowedValues = {"FARGATE", "EC2"},
        required = true,
        order = 30
    )
    public RuntimeType runtime;

    /** Topology type — user-selectable, not just an auto-derived display value, so a future
     *  topology (or an app implementing more than one applicable interface) isn't locked out of
     *  being chosen explicitly. No {@code allowedValues} override here on purpose: leaving it
     *  unset lets the schema builder enumerate every {@link TopologyType} constant automatically —
     *  a hardcoded single-value override would hide CMS_SERVICE/JENKINS_SERVICE/S3_WEBSITE from
     *  the wizard entirely, locking out a CmsSpec app's actual topology even though {@link
     *  DeploymentContextPreparer} already defaults to it correctly. */
    @ConfigField(
        displayName = "Topology",
        description = "Deployment topology pattern",
        category = "basic",
        order = 40
    )
    public TopologyType topology;

    /** Security profile (DEV, STAGING, PRODUCTION) */
    @ConfigField(
        displayName = "Security Profile",
        description = "Security profile determines compliance requirements and defaults",
        category = "security",
        allowedValues = {"DEV", "STAGING", "PRODUCTION"},
        required = true,
        order = 10
    )
    public SecurityProfile securityProfile = SecurityProfile.DEV;

    // ========== Network Configuration ==========

    /** Network mode for VPC topology */
    @ConfigField(
        displayName = "Network Mode",
        description = "VPC topology. Private with NAT is recommended for production; public is suitable for development; isolated requires VPC endpoints.",
        category = "network",
        allowedValues = {"public", "private-with-nat", "isolated"},
        example = "private-with-nat",
        order = 10
    )
    public NetworkMode networkMode = NetworkMode.PUBLIC;

    /** Load balancer type */
    @ConfigField(
        displayName = "Load Balancer Type",
        description = "ALB (HTTP/HTTPS) or NLB (TCP/UDP) - ALB required for OIDC",
        category = "network",
        order = 20
    )
    public LoadBalancerType lbType = LoadBalancerType.ALB;

    /** Create Route53 hosted zone */
    @ConfigField(
        displayName = "Create Route53 Zone",
        description = "Create new Route53 hosted zone for the domain",
        category = "domain",
        order = 40
    )
    public Boolean createZone = false;

    /** Enable VPC flow logs */
    @ConfigField(
        displayName = "Enable Flow Logs",
        description = "Enable VPC flow logs for network traffic analysis",
        category = "network",
        order = 30
    )
    public Boolean enableFlowlogs = null;  // Null means security profile determines default

    /** Enable AWS WAF */
    @ConfigField(
        displayName = "Enable WAF",
        description = "Enable AWS Web Application Firewall for ALB protection",
        category = "network",
        visibleWhen = "lbType == alb",
        order = 40
    )
    public Boolean wafEnabled;

    /** HTTPS-only mode (no HTTP listener when SSL enabled) */
    @ConfigField(
        displayName = "HTTPS Strict Mode",
        description = "Disable HTTP listener when SSL enabled (no HTTP→HTTPS redirect)",
        category = "network",
        visibleWhen = "enableSsl == true",
        order = 45
    )
    public Boolean httpsStrictEnabled;

    /** Enable ALB access logs to S3. Null means the security profile decides. */
    @ConfigField(
        displayName = "ALB Access Logging",
        description = "Log all ALB requests to S3 for auditing",
        category = "network",
        visibleWhen = "lbType == alb",
        order = 50
    )
    public Boolean albAccessLogging;

    /** Enable CloudFront CDN */
    @ConfigField(
        displayName = "Enable CloudFront",
        description = "Enable CloudFront CDN for global content delivery",
        category = "network",
        tags = {FieldTag.BILLING_IMPACT},
        order = 60
    )
    @JsonAlias("cloudfront")
    public Boolean cloudfrontEnabled;

    /** CIDR for bastion/VPN SSH access */
    @ConfigField(
        displayName = "Bastion CIDR",
        description = "CIDR for bastion/VPN SSH access (PRODUCTION profile)",
        category = "network",
        pattern = "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$",
        example = "10.0.1.0/24",
        order = 70
    )
    public String bastionCidr = "10.0.1.0/24";

    // ========== Resource Configuration ==========

    /** Minimum instance capacity for auto-scaling */
    @ConfigField(
        displayName = "Min Instance Capacity",
        description = "Minimum number of instances for auto-scaling",
        category = "resources",
        min = 1,
        max = 100,
        order = 10
    )
    public int minInstanceCapacity = 1;

    /** Maximum instance capacity for auto-scaling */
    @ConfigField(
        displayName = "Max Instance Capacity",
        description = "Maximum number of instances for auto-scaling",
        category = "resources",
        min = 1,
        max = 100,
        validators = {"CapacityValidator"},
        tags = {FieldTag.BILLING_IMPACT},
        order = 20
    )
    public int maxInstanceCapacity = 1;

    /** CPU target utilization percentage for auto-scaling */
    @ConfigField(
        displayName = "CPU Target Utilization (%)",
        description = "Target CPU utilization for auto-scaling triggers",
        category = "resources",
        min = 10,
        max = 90,
        visibleWhen = "maxInstanceCapacity > 1",
        order = 30
    )
    public int cpuTargetUtilization = 60;

    /** Fargate CPU units (256, 512, 1024, 2048, 4096) */
    @ConfigField(
        displayName = "CPU (Fargate units)",
        description = "Fargate CPU units: 256=0.25vCPU, 512=0.5vCPU, 1024=1vCPU, etc.",
        category = "resources",
        allowedValues = {"256", "512", "1024", "2048", "4096", "8192", "16384"},
        visibleWhen = "runtime == FARGATE",
        validators = {"FargateCpuMemoryValidator"},
        tags = {FieldTag.BILLING_IMPACT},
        order = 40
    )
    public int cpu = 1024;

    /** Fargate memory in MB */
    @ConfigField(
        displayName = "Memory (MB)",
        description = "Container memory in MB (must be valid for CPU selection)",
        category = "resources",
        min = 512,
        max = 122880,
        visibleWhen = "runtime == FARGATE",
        validators = {"FargateCpuMemoryValidator"},
        tags = {FieldTag.BILLING_IMPACT},
        order = 50
    )
    public int memory = 2048;

    /** EC2 instance type (e.g., "t3.micro", "t3.small") */
    @ConfigField(
        displayName = "EC2 Instance Type",
        description = "EC2 instance size for compute capacity",
        category = "resources",
        // Choices come from InstanceCatalog.ec2InstanceTypes().
        visibleWhen = "runtime == EC2",
        defaultFrom = "defaultInstanceType",
        tags = {FieldTag.BILLING_IMPACT},
        order = 60
    )
    public String instanceType = "t3.micro";

    /** Override container image tag */
    @ConfigField(
        displayName = "Container Image",
        description = "Override container image tag (e.g., 'v1.2.3' or '2024.1')",
        category = "resources",
        example = "v1.2.3",
        order = 65
    )
    public String containerImage;

    // ========== Storage Configuration ==========

    /** Retain EFS/EBS volumes on stack deletion */
    @ConfigField(
        displayName = "Retain Storage",
        description = "Retain EFS/EBS volumes on stack deletion (agnostic - works for any workload)",
        category = "storage",
        order = 10
    )
    public Boolean retainStorage = false;

    /** Reuse existing EFS by ID (for disaster recovery workflows) */
    @ConfigField(
        displayName = "Existing File System ID",
        description = "Reuse existing EFS by ID (for disaster recovery workflows)",
        category = "storage",
        example = "fs-12345678",
        order = 20
    )
    public String existingFileSystemId;

    /** S3 bucket for artifacts */
    @ConfigField(
        displayName = "Artifacts Bucket",
        description = "S3 bucket name for build artifacts",
        category = "storage",
        order = 30
    )
    public String artifactsBucket;

    /** S3 prefix for artifacts */
    @ConfigField(
        displayName = "Artifacts Prefix",
        description = "S3 prefix for build artifacts",
        category = "storage",
        example = "jenkins/job/${JOB_NAME}/${BUILD_NUMBER}",
        order = 40
    )
    public String artifactsPrefix = "jenkins/job/${JOB_NAME}/${BUILD_NUMBER}";

    // ========== Authentication Configuration ==========

    /** Authentication mode */
    @ConfigField(
        displayName = "Authentication Mode",
        description = "Authentication integration type: ALB-level OIDC, application-level, or none",
        category = "security",
        visibleWhen = "supportsOidc",
        order = 20
    )
    public AuthMode authMode = AuthMode.NONE;

    /** OIDC provider (none, cognito, identity-center, external-idp) */
    @ConfigField(
        displayName = "OIDC Provider",
        description = "Identity provider for OIDC authentication",
        category = "security",
        allowedValues = {"none", "cognito", "identity-center", "external-idp", "cloudforge-manager"},
        visibleWhen = "authMode != none",
        order = 30
    )
    public String oidcProvider = "none";

    /** URL path patterns that require authentication, replacing the application's own default
     *  list ({@code ApplicationSpec#protectedPaths()}) when set. Consumed by both
     *  {@code CognitoAuthenticationFactory} and {@code OidcAuthenticationFactory} -- see their
     *  {@code calculateEffectiveProtectedPaths()}: unset/empty means "use the application's
     *  default protected paths", not "protect nothing". */
    @ConfigField(
        displayName = "Protected Paths",
        description = "URL path patterns requiring authentication (replaces the application's defaults)",
        category = "security",
        visibleWhen = "authMode != none",
        example = "/admin/*,/api/*",
        order = 31
    )
    public List<String> protectedPaths = null;

    /** Additional URL path patterns requiring authentication, added on top of whichever base list
     *  ({@link #protectedPaths} or the application default) is in effect. */
    @ConfigField(
        displayName = "Additional Protected Paths",
        description = "Extra URL path patterns requiring authentication, added to the base list",
        category = "security",
        visibleWhen = "authMode != none",
        example = "/reports/*",
        order = 32
    )
    public List<String> additionalProtectedPaths = null;

    /** URL path patterns explicitly excluded from authentication, removed from the effective
     *  protected-paths list after {@link #protectedPaths}/{@link #additionalProtectedPaths} and
     *  the application's own defaults are combined. */
    @ConfigField(
        displayName = "Public Paths",
        description = "URL path patterns excluded from authentication",
        category = "security",
        visibleWhen = "authMode != none",
        example = "/health,/public/*",
        order = 33
    )
    public List<String> publicPaths = null;

    /** Auto-provision new Cognito User Pool */
    @ConfigField(
        displayName = "Auto-Provision Cognito",
        description = "Create a new Cognito User Pool for authentication",
        category = "security",
        visibleWhen = "oidcProvider == cognito",
        order = 40
    )
    public Boolean cognitoAutoProvision = false;

    /** Cognito User Pool name */
    @ConfigField(
        displayName = "Cognito User Pool Name",
        description = "Name for the Cognito User Pool",
        category = "security",
        visibleWhen = "cognitoAutoProvision == true",
        order = 50
    )
    public String cognitoUserPoolName = null;

    /** Cognito domain prefix (must be globally unique) */
    @ConfigField(
        displayName = "Cognito Domain Prefix",
        description = "Globally unique prefix for Cognito hosted UI domain",
        category = "security",
        pattern = "^[a-z][a-z0-9-]{0,62}$",
        visibleWhen = "cognitoAutoProvision == true",
        order = 60
    )
    public String cognitoDomainPrefix = null;

    /** Enable MFA for Cognito */
    @ConfigField(
        displayName = "Enable MFA",
        description = "Require multi-factor authentication for Cognito users",
        category = "security",
        visibleWhen = "cognitoAutoProvision == true",
        order = 70
    )
    public Boolean cognitoMfaEnabled = false;

    /** Cognito MFA method */
    @ConfigField(
        displayName = "MFA Method",
        description = "MFA method: totp (authenticator app), sms, or both",
        category = "security",
        allowedValues = {"totp", "sms", "both"},
        visibleWhen = "cognitoMfaEnabled == true",
        order = 75
    )
    public String cognitoMfaMethod = "both";

    /**
     * Overrides the security-profile default for whether users can self-register on the
     * Cognito hosted UI (development defaults to allowed, staging/production to admin-only).
     * Left {@code null}, the profile default applies unchanged. Self-signup is not enforced
     * by any compliance framework rule today — a self-registered user still gets no group
     * membership and resolves to the lowest-privilege role — but enabling it while a compliance
     * framework is active (advisory or enforce) logs a warning at synthesis time, since open
     * self-registration into an ops panel is a common audit finding independent of what role
     * it grants.
     */
    @ConfigField(
        displayName = "Enable Self-Signup",
        description = "Override the security profile's default for self-service Cognito signup",
        category = "security",
        visibleWhen = "cognitoAutoProvision == true",
        order = 78
    )
    public Boolean cognitoSelfSignupEnabled = null;

    /** Create admin and user groups in Cognito */
    @ConfigField(
        displayName = "Create User Groups",
        description = "Create admin and user groups in Cognito",
        category = "security",
        visibleWhen = "cognitoAutoProvision == true",
        order = 80
    )
    public Boolean cognitoCreateGroups = true;

    /** Admin group name */
    @ConfigField(
        displayName = "Admin Group Name",
        description = "Name for the Cognito admin group",
        category = "security",
        visibleWhen = "cognitoCreateGroups == true",
        order = 90
    )
    public String cognitoAdminGroupName = null;

    /** User group name */
    @ConfigField(
        displayName = "User Group Name",
        description = "Name for the Cognito users group",
        category = "security",
        visibleWhen = "cognitoCreateGroups == true",
        order = 100
    )
    public String cognitoUserGroupName = null;

    /** Initial admin email address */
    @ConfigField(
        displayName = "Initial Admin Email",
        description = "Email address for the first admin user",
        category = "security",
        pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
        visibleWhen = "cognitoAutoProvision == true",
        example = "admin@example.com",
        order = 110
    )
    public String cognitoInitialAdminEmail = null;

    /** Initial admin phone number (E.164 format) */
    @ConfigField(
        displayName = "Initial Admin Phone",
        description = "Phone number for admin user (E.164 format: +1234567890)",
        category = "security",
        pattern = "^\\+[1-9]\\d{1,14}$",
        visibleWhen = "cognitoMfaEnabled == true",
        example = "+15551234567",
        order = 120
    )
    public String cognitoInitialAdminPhone = null;

    /** Existing Cognito User Pool ID */
    @ConfigField(
        displayName = "Existing User Pool ID",
        description = "ID of existing Cognito User Pool to use",
        category = "security",
        visibleWhen = "oidcProvider == cognito && cognitoAutoProvision == false",
        order = 130
    )
    public String cognitoUserPoolId = null;

    /** Existing Cognito App Client ID */
    @ConfigField(
        displayName = "Existing App Client ID",
        description = "ID of existing Cognito App Client to use",
        category = "security",
        visibleWhen = "oidcProvider == cognito && cognitoAutoProvision == false",
        order = 140
    )
    public String cognitoAppClientId = null;

    /** Manually configured Cognito app client ID, for a client that wasn't auto-provisioned.
     *  Distinct from {@link #cognitoAppClientId} above (used to reference an *existing user
     *  pool's* client when {@code cognitoAutoProvision == false}) -- this is read directly by
     *  {@code ApplicationOidcFactory} as a manual-configuration fallback. */
    @ConfigField(
        displayName = "Cognito User Pool Client ID (manual)",
        description = "Manually configured Cognito app client ID",
        category = "security",
        order = 141
    )
    public String cognitoUserPoolClientId = null;

    // ========== External OIDC Configuration ==========

    /** OIDC issuer URL */
    @ConfigField(
        displayName = "OIDC Issuer URL",
        description = "Issuer URL from your identity provider (e.g., https://login.example.com)",
        category = "security",
        visibleWhen = "oidcProvider == external-idp",
        pattern = "^https://.*$",
        example = "https://login.example.com",
        order = 200
    )
    public String oidcIssuer = null;

    /** CloudForge Manager's own public URL, used as the OIDC issuer when this app trusts a
     *  CloudForge Manager install as its identity provider (see
     *  com.cloudforgeci.manager.auth.oidcprovider's own package javadoc). Authorization/token/
     *  userinfo endpoints are computed from this one URL (CloudForge Manager's OIDC-provider
     *  endpoints are at fixed paths — /oauth2/authorize, /oauth2/token, /userinfo — so unlike
     *  the generic external-idp option above, only the base URL needs entering here, not each
     *  endpoint separately). */
    @ConfigField(
        displayName = "CloudForge Manager URL",
        description = "Public URL of the CloudForge Manager install this app trusts as its identity provider",
        category = "security",
        visibleWhen = "oidcProvider == cloudforge-manager",
        pattern = "^https?://.*$",
        example = "https://manager.example.com",
        order = 195
    )
    public String cloudforgeManagerIssuerUrl = null;

    /** OIDC authorization endpoint */
    @ConfigField(
        displayName = "Authorization Endpoint",
        description = "OIDC authorization endpoint URL",
        category = "security",
        visibleWhen = "oidcProvider == external-idp",
        pattern = "^https://.*$",
        order = 210
    )
    public String oidcAuthorizationEndpoint = null;

    /** OIDC token endpoint */
    @ConfigField(
        displayName = "Token Endpoint",
        description = "OIDC token endpoint URL",
        category = "security",
        visibleWhen = "oidcProvider == external-idp",
        pattern = "^https://.*$",
        order = 220
    )
    public String oidcTokenEndpoint = null;

    /** OIDC user info endpoint */
    @ConfigField(
        displayName = "User Info Endpoint",
        description = "OIDC user info endpoint URL",
        category = "security",
        visibleWhen = "oidcProvider == external-idp",
        pattern = "^https://.*$",
        order = 230
    )
    public String oidcUserInfoEndpoint = null;

    /** OIDC client ID — shared by external-idp and cloudforge-manager, both providers where an
     *  admin registers this app's client credentials by hand (external IdP's own console, or
     *  CloudForge Manager's Trusted Apps settings page) rather than CloudForge auto-provisioning
     *  them, so the field means the same thing either way. */
    @ConfigField(
        displayName = "OIDC Client ID",
        description = "Client ID from your identity provider",
        category = "security",
        visibleWhen = "oidcProvider == external-idp || oidcProvider == cloudforge-manager",
        order = 240
    )
    public String oidcClientId = null;

    /** OIDC client secret name in Secrets Manager — same sharing rationale as {@link
     *  #oidcClientId}. */
    @ConfigField(
        displayName = "Client Secret (Secrets Manager)",
        description = "Name of the secret in AWS Secrets Manager containing the client secret",
        category = "security",
        visibleWhen = "oidcProvider == external-idp || oidcProvider == cloudforge-manager",
        sensitive = true,
        order = 250
    )
    public String oidcClientSecretName = null;

    // ========== Optional Ports Configuration ==========
    // These enable optional services on applications that support them.
    // Ports are NOT exposed by default - must be explicitly enabled.

    /** Enable JNLP build agent port (Jenkins: 50000) */
    @ConfigField(
        displayName = "Enable Build Agents",
        description = "Enable JNLP build agent port for distributed builds (Jenkins: 50000)",
        category = "ports",
        visibleWhen = "supportsAgents",
        order = 10
    )
    public boolean enableAgents = false;

    /** Enable Git SSH port (GitLab: 22, Gitea: 2222) */
    @ConfigField(
        displayName = "Enable Git SSH",
        description = "Enable Git SSH access for repository cloning (GitLab: 22, Gitea: 2222)",
        category = "ports",
        visibleWhen = "supportsSsh",
        order = 20
    )
    public boolean enableSsh = false;

    /** Enable SMTP email port (Mattermost: 587) */
    @ConfigField(
        displayName = "Enable SMTP",
        description = "Enable SMTP port for outbound email (port 587)",
        category = "ports",
        visibleWhen = "supportsSmtp",
        order = 30
    )
    public boolean enableSmtp = false;

    /** Enable SMTP TLS email port (Mattermost: 465) */
    @ConfigField(
        displayName = "Enable SMTPS",
        description = "Enable SMTP over TLS port for secure outbound email (port 465)",
        category = "ports",
        visibleWhen = "supportsSmtps",
        order = 40
    )
    public boolean enableSmtps = false;

    /** Enable clustering ports (Mattermost: 8074-8075, Vault: 8201) */
    @ConfigField(
        displayName = "Enable Clustering",
        description = "Enable inter-node clustering ports for high availability",
        category = "ports",
        visibleWhen = "supportsClustering",
        order = 50
    )
    public boolean enableClustering = false;

    /** Enable container registry port (GitLab: 5050, Nexus: 5000-5002) */
    @ConfigField(
        displayName = "Enable Docker Registry",
        description = "Enable container registry ports (GitLab: 5050, Nexus: 5000-5002)",
        category = "ports",
        visibleWhen = "supportsDockerRegistry",
        order = 60
    )
    public boolean enableDockerRegistry = false;

    /** Enable Prometheus metrics port (GitLab: 9090) */
    @ConfigField(
        displayName = "Enable Metrics Port",
        description = "Enable Prometheus metrics endpoint port (typically 9090)",
        category = "ports",
        visibleWhen = "supportsMetrics",
        order = 70
    )
    public boolean enableMetrics = false;

    /** Enable Notary content trust port (Harbor: 4443) */
    @ConfigField(
        displayName = "Enable Notary",
        description = "Enable Docker Content Trust Notary port (Harbor: 4443)",
        category = "ports",
        visibleWhen = "supportsNotary",
        order = 80
    )
    public boolean enableNotary = false;

    /** Enable Trivy vulnerability scanner port (Harbor: 8080) */
    @ConfigField(
        displayName = "Enable Trivy Scanner",
        description = "Enable Trivy vulnerability scanner port (Harbor: 8080)",
        category = "ports",
        visibleWhen = "supportsTrivy",
        order = 90
    )
    public boolean enableTrivy = false;

    /** Enable Redis Sentinel port (Redis: 26379) */
    @ConfigField(
        displayName = "Enable Sentinel",
        description = "Enable Redis Sentinel port for HA monitoring (port 26379)",
        category = "ports",
        visibleWhen = "supportsSentinel",
        order = 100
    )
    public boolean enableSentinel = false;

    /** Enable Redis Cluster bus port (Redis: 16379) */
    @ConfigField(
        displayName = "Enable Cluster Bus",
        description = "Enable Redis Cluster bus port for cluster gossip (port 16379)",
        category = "ports",
        visibleWhen = "supportsClusterBus",
        order = 110
    )
    public boolean enableCluster = false;

    // ========== IAM Identity Center Configuration ==========

    /** Auto-provision SAML application in IAM Identity Center */
    @ConfigField(
        displayName = "Auto-Provision Identity Center",
        description = "Create SAML application in IAM Identity Center",
        category = "security",
        visibleWhen = "oidcProvider == identity-center",
        order = 260
    )
    public Boolean autoProvisionIdentityCenter = false;

    /** IAM Identity Center (SSO) Instance ARN */
    @ConfigField(
        displayName = "SSO Instance ARN",
        description = "ARN of IAM Identity Center instance",
        category = "security",
        visibleWhen = "oidcProvider == identity-center",
        order = 270
    )
    public String ssoInstanceArn = null;

    /** SSO Group ID */
    @ConfigField(
        displayName = "SSO Group ID",
        description = "IAM Identity Center group UUID",
        category = "security",
        visibleWhen = "oidcProvider == identity-center",
        order = 275
    )
    public String ssoGroupId = null;

    /** SSO Target Account ID */
    @ConfigField(
        displayName = "SSO Target Account ID",
        description = "12-digit AWS account ID for SSO target",
        category = "security",
        pattern = "^\\d{12}$",
        visibleWhen = "oidcProvider == identity-center",
        order = 278
    )
    public String ssoTargetAccountId = null;

    /** Identity Center group name for user assignment */
    @ConfigField(
        displayName = "Identity Center Group",
        description = "IAM Identity Center group for user assignment",
        category = "security",
        visibleWhen = "oidcProvider == identity-center",
        order = 280
    )
    public String identityCenterGroupName = null;

    // ========== Database Configuration ==========

    /**
     * Provision RDS database for application.
     * Only shown for applications with optional database support (e.g., Metabase, Grafana).
     * Applications requiring database (e.g., Mattermost, GitLab) always provision one.
     */
    @ConfigField(
        displayName = "Provision RDS Database",
        description = "Create managed RDS database for high availability and automatic backups",
        category = "database",
        visibleWhen = "supportsDatabase && !requiresDatabase",
        required = false,
        order = 10
    )
    public Boolean provisionDatabase = false;

    /**
     * Database engine (e.g., postgres, mysql, mariadb).
     * Default comes from ApplicationSpec.databaseRequirement().engine()
     *
     * <p><b>Deliberately no static default here (nor on the other {@code database*} fields below
     * with a {@code defaultFrom}):</b> {@link com.cloudforge.core.config.DeploymentContextPreparer}
     * only applies a field's {@code defaultFrom}-resolved ApplicationSpec value when the field is
     * currently null/blank, so a non-null Java initializer would always look "already set" and
     * block that resolution. Mixing a caller-supplied engine with a static default version can
     * then produce an invalid combination (e.g. {@code databaseEngine=mysql} with a Postgres
     * version yields a nonexistent "mysql15" RDS parameter group family). {@code cloudforge-api}'s
     * {@code ApplicationFactory} merges these fields against
     * {@code ApplicationSpec.databaseRequirement()} with the same null-check pattern.</p>
     */
    @ConfigField(
        displayName = "Database Engine",
        description = "RDS database engine type",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        allowedValues = {"postgres", "mysql", "mariadb"},
        defaultFrom = "databaseRequirement().engine",
        example = "postgres",
        order = 20
    )
    public String databaseEngine;

    /**
     * Database engine version.
     * Default comes from ApplicationSpec.databaseRequirement().effectiveDefaultVersion(). The
     * application's declared version is a minimum: a higher version may be chosen, a lower one is
     * rejected, and a version above the application's tested maximum is accepted with a warning.
     */
    @ConfigField(
        displayName = "Database Version",
        description = "Database engine version (e.g., 15 for PostgreSQL 15)",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        example = "15",
        defaultFrom = "databaseRequirement().effectiveDefaultVersion",
        order = 30
    )
    public String databaseVersion;

    /**
     * RDS instance class (e.g., db.t3.small, db.m5.large).
     * DESTRUCTIVE: Changing this requires resource replacement.
     * BILLING_IMPACT: Larger instances cost more.
     */
    @ConfigField(
        displayName = "Database Instance Class",
        description = "RDS instance type - larger instances provide more CPU/memory",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        // Choices come from InstanceCatalog.databaseInstanceClasses().
        example = "db.t3.small",
        defaultFrom = "databaseRequirement().instanceClass",
        tags = {FieldTag.DESTRUCTIVE, FieldTag.BILLING_IMPACT},
        order = 40
    )
    public String databaseInstanceClass;

    /**
     * Allocated storage in GB.
     * BILLING_IMPACT: More storage costs more.
     */
    @ConfigField(
        displayName = "Database Storage (GB)",
        description = "Allocated storage for RDS database in GB",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        min = 20,
        max = 65536,
        example = "100",
        defaultFrom = "databaseRequirement().allocatedStorageGB",
        tags = {FieldTag.BILLING_IMPACT},
        order = 50
    )
    public Integer databaseAllocatedStorageGB;

    /**
     * Enable Multi-AZ deployment for high availability.
     * BILLING_IMPACT: Multi-AZ doubles database costs.
     *
     * <p>Deliberately no static default: any non-null value is treated as an explicit override of
     * the security profile, so a {@code false} initializer would keep PRODUCTION and
     * compliance-driven Multi-AZ from ever applying. When unset, the profile decides.</p>
     */
    @ConfigField(
        displayName = "Multi-AZ Deployment",
        description = "Deploy database across multiple availability zones for high availability",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        tags = {FieldTag.BILLING_IMPACT},
        order = 60
    )
    public Boolean databaseMultiAz;

    /**
     * Automatic minor-version upgrades for the RDS instance.
     *
     * <p>Deliberately no static default: unset lets RdsFactory fall back to its
     * security-profile default (PRODUCTION only). A {@code false} initializer would
     * make an explicit override indistinguishable from "not set".</p>
     */
    @ConfigField(
        displayName = "RDS Auto Minor Version Upgrade",
        description = "Apply RDS engine minor-version security patches automatically",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        order = 61
    )
    public Boolean rdsAutoMinorVersionUpgrade;

    /**
     * RDS Performance Insights (query-level performance monitoring).
     *
     * <p>Deliberately no static default -- see {@link #databaseMultiAz}. RdsFactory
     * also skips this regardless of override on instance classes too small to support it
     * (e.g. db.t3.micro).</p>
     */
    @ConfigField(
        displayName = "RDS Performance Insights",
        description = "Enable Performance Insights for query-level RDS monitoring",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        order = 62
    )
    public Boolean performanceInsightsEnabled;

    /**
     * RDS Enhanced Monitoring (OS-level metrics at a configurable interval).
     *
     * <p>Deliberately no static default -- see {@link #databaseMultiAz}.</p>
     */
    @ConfigField(
        displayName = "RDS Enhanced Monitoring",
        description = "Enable RDS Enhanced Monitoring for real-time OS metrics",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        order = 63
    )
    public Boolean rdsEnhancedMonitoringEnabled;

    /**
     * Optional number of RDS read replicas. When unset, an application may provide
     * its own default (CloudForge Manager defaults to one); zero explicitly disables
     * replicas for applications that support them.
     */
    @ConfigField(
        displayName = "Database Read Replicas",
        description = "Number of read-only RDS replicas to deploy (0 disables replicas)",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        min = 0,
        max = 5,
        example = "1",
        tags = {FieldTag.BILLING_IMPACT},
        order = 65
    )
    public Integer databaseReadReplicaCount = null;

    /**
     * Database name.
     * IMMUTABLE: Cannot be changed after creation.
     */
    @ConfigField(
        displayName = "Database Name",
        description = "Initial database name to create",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        pattern = "^[a-zA-Z][a-zA-Z0-9_]{0,62}$",
        example = "appdb",
        defaultFrom = "databaseRequirement().databaseName",
        tags = {FieldTag.IMMUTABLE},
        order = 70
    )
    public String databaseName;

    /**
     * Backup retention period in days.
     * Compliance frameworks may override: PCI-DSS (90 days), HIPAA (30 days), SOC2 (14 days).
     */
    @ConfigField(
        displayName = "Backup Retention (Days)",
        description = "Number of days to retain automated backups (0 = disabled)",
        category = "database",
        visibleWhen = "provisionDatabase",
        dependsOn = "provisionDatabase",
        min = 0,
        max = 35,
        example = "7",
        order = 80
    )
    public Integer databaseBackupRetentionDays = 7;

    // ========== Redis Session Store (CloudForge Manager) ==========

    /**
     * Provision an ElastiCache Redis cluster and bind CloudForge Manager's own container to it
     * as a shared session store (CFC_MANAGER_SESSION_MODE=redis), so every Manager instance
     * behind the same ALB recognizes sessions any of the others created — the missing piece for
     * running Manager itself horizontally scaled. Only {@code ApplicationFactory} acts on this,
     * and only when {@code applicationId == "cloudforge-manager"}; {@code visibleWhen} below keeps
     * it out of every other application's deploy form for the same reason. Requires RDS
     * (embedded H2 isn't safe to share across instances) — see {@code
     * ManagerDeploymentPreset.rdsWithRedisSessions(...)} in cloudforge-manager-deployment, the
     * typed pairing this mirrors. Single-instance Manager (the common case) needs nothing beyond
     * the in-memory default and should leave this false.
     */
    @ConfigField(
        displayName = "Redis Session Store",
        description = "Provision ElastiCache Redis and share sessions across every CloudForge "
            + "Manager instance — required for running Manager itself horizontally scaled behind an ALB",
        category = "database",
        visibleWhen = "applicationId == \"cloudforge-manager\" && provisionDatabase",
        dependsOn = "provisionDatabase",
        tags = {FieldTag.BILLING_IMPACT},
        order = 90
    )
    public Boolean provisionManagerRedisSessions = false;

    /**
     * Provision a dedicated AWS Secrets Manager entry holding the AES cipher key CloudForge
     * Manager uses to encrypt cross-account connection secrets (external IDs) at rest — see
     * {@code SecretCipher}/{@code AesGcmSecretCipher} in cloudforge-manager. Injected into
     * Manager's own ECS task as {@code CFC_MANAGER_ACCOUNT_SECRET_KEY} via {@code
     * ecs.Secret.fromSecretsManager(...)}, mirroring how {@code CFC_MANAGER_DATABASE_PASSWORD}
     * is already delivered — never a literal value in the task definition. Only {@code
     * ApplicationFactory} acts on this, and only when {@code applicationId ==
     * cloudforge-manager}. Defaults to {@code true} (unlike {@code
     * provisionManagerRedisSessions}) because without it Manager falls back to {@code
     * PlaintextSecretCipher} — leaving it on is the secure-by-default choice for any AWS
     * deployment of Manager, not an opt-in scaling feature like Redis. Independent of {@code
     * provisionDatabase}: even an embedded-H2 Manager deployment benefits from encrypting the
     * secrets it holds.
     */
    @ConfigField(
        displayName = "Account Connection Cipher Key",
        description = "Provision a Secrets Manager entry for the AES key CloudForge Manager uses "
            + "to encrypt cross-account connection secrets — leave enabled unless you are "
            + "supplying CFC_MANAGER_ACCOUNT_SECRET_KEY through another mechanism",
        category = "database",
        visibleWhen = "applicationId == \"cloudforge-manager\"",
        tags = {FieldTag.BILLING_IMPACT},
        order = 91
    )
    public Boolean provisionManagerAccountCipherKey = true;

    /**
     * A customer's LicenseSeat license key ({@code CFC-XXXX-XXXX-XXXX-XXXX}), injected at deploy
     * time so an install activates immediately instead of requiring a follow-up visit to the
     * owner-only License settings screen after the stack comes up. When set, {@code
     * ApplicationFactory} provisions a dedicated Secrets Manager entry for it and {@code
     * ContainerFactory} binds that as {@code CFC_MANAGER_LICENSESEAT_LICENSE_KEY} — the exact
     * env var {@code ManagerRuntimeConfiguration} reads for its environment-supplied license key
     * (see its {@code LicenseSeat} record javadoc, cloudforge-manager). Purely additive: the in-app License
     * settings screen still works, and a key activated through it still takes precedence (see
     * {@code LicenseKeyStore}, cloudforge-manager) — this only seeds the initial value.
     *
     * <p>{@code category = "database"} despite not being a database setting: it sits right next
     * to {@code provisionManagerAccountCipherKey} above for the same reason that field does —
     * {@code InteractiveDeployer.configureDatabaseOptions()} is the one place that currently
     * discovers and prompts for manager-only fields by reflection ({@code
     * ConfigurationIntrospector.discoverVisibleFields(..., "database")}); a "security"
     * category discovery pass would also surface ~30 unrelated existing security fields that
     * were never meant for CLI prompting.
     */
    @ConfigField(
        displayName = "License Key",
        description = "LicenseSeat customer license key (CFC-XXXX-XXXX-XXXX-XXXX) to activate this "
            + "install with on first boot, delivered via a dedicated Secrets Manager entry",
        category = "database",
        visibleWhen = "applicationId == \"cloudforge-manager\"",
        sensitive = true,
        order = 92
    )
    public String managerLicenseKey;

    /** Enable RDS deletion protection remediation */
    @ConfigField(
        displayName = "RDS Deletion Protection Remediation",
        description = "Enable automatic remediation of RDS deletion protection compliance violations",
        category = "compliance",
        visibleWhen = "provisionDatabase && awsConfigEnabled",
        dependsOn = "awsConfigEnabled",
        order = 400
    )
    public Boolean enableRdsDeletionProtectionRemediation = false;

    /** Enable RDS auto minor version upgrade remediation */
    @ConfigField(
        displayName = "RDS Auto Minor Version Upgrade Remediation",
        description = "Enable automatic remediation of RDS auto minor version upgrade compliance violations",
        category = "compliance",
        visibleWhen = "provisionDatabase && awsConfigEnabled",
        dependsOn = "awsConfigEnabled",
        order = 410
    )
    public Boolean enableRdsAutoMinorVersionUpgradeRemediation = false;

    // ========== Compliance Configuration ==========

    /**
     * Compliance frameworks to enable.
     *
     * <p>Supports comma-separated string format in JSON for backward compatibility:
     * <pre>{"complianceFrameworks": "soc2,pci-dss,hipaa"}</pre>
     *
     * <p>In Java code, use the type-safe List:
     * <pre>config.complianceFrameworks.contains(ComplianceFrameworkType.HIPAA)</pre>
     */
    @ConfigField(
        displayName = "Compliance Frameworks",
        description = "Compliance frameworks to enable (soc2, pci-dss, hipaa, gdpr)",
        category = "compliance",
        // allowedValues lets UIs render checkboxes with the exact expected tokens (matching
        // ComplianceFrameworkType's JSON values) instead of a free-text list prone to typos.
        allowedValues = {"soc2", "pci-dss", "hipaa", "gdpr"},
        example = "soc2,pci-dss",
        order = 300
    )
    @JsonDeserialize(using = ComplianceFrameworkListConverter.Deserializer.class)
    @JsonSerialize(using = ComplianceFrameworkListConverter.Serializer.class)
    public List<ComplianceFrameworkType> complianceFrameworks = new ArrayList<>();

    /**
     * Raw compliance-framework token override for an on-demand advisory check (see
     * {@code CloudForgeSynthesizer#synthesizeAdvisoryDryRun}) -- lets a caller ask cdk-nag /
     * FrameworkRules to validate against tokens with no {@link ComplianceFrameworkType} entry at
     * all (e.g. {@code "ISO-27001"}, or {@code "AWS-BEST-PRACTICES"} for cdk-nag's own generic
     * {@code AwsSolutionsChecks} fallback pack — see {@code SecurityRules#mapFrameworkToNagPack}'s
     * {@code default} case), without extending the license-gated {@link #complianceFrameworks}
     * enum real deploys use. Comma-separated, same format {@link #complianceFrameworks} itself
     * serializes to.
     *
     * <p>Deliberately NOT part of {@link #toContextMap}'s output: the "cfc" context map it builds
     * gets round-tripped back through a strict {@code DeploymentConfig} deserialization inside
     * {@code DeploymentContext.from} (jsii construct-tree plumbing), and that round trip enforces
     * the same {@link ComplianceFrameworkType} enum this field exists specifically to bypass —
     * feeding it through that key would just re-throw the same "unknown compliance framework"
     * error one layer later. {@code CloudForgeSynthesizer#synthesize} instead seeds this under its
     * own separate, sibling top-level CDK context key (not nested in "cfc" at all), and {@code
     * SecurityRules#install} reads it directly off the construct tree rather than through {@code
     * DeploymentContext}. {@code transient}/{@code @JsonIgnore} since it must never round-trip
     * through a saved deployment context the way every other field here does.</p>
     */
    @JsonIgnore
    public transient String complianceFrameworksRawOverride;

    /** Legacy single-framework override, superseded by {@link #complianceFrameworks} (which
     *  supports multiple). Kept for backward compatibility with older deployment contexts. */
    @ConfigField(
        displayName = "Audit Manager Framework ID (legacy)",
        description = "Single compliance framework to enable (legacy -- prefer complianceFrameworks)",
        category = "compliance",
        order = 301
    )
    public String auditManagerFrameworkId = null;

    /**
     * Compliance validation mode controlling how validation failures are handled.
     */
    @ConfigField(
        displayName = "Compliance Mode",
        description = "How to handle compliance validation failures",
        category = "compliance",
        allowedValues = {"enforce", "advisory", "disabled"},
        order = 310
    )
    public ComplianceMode complianceMode;  // null = use defaultForProfile(securityProfile)

    /** CloudWatch Logs retention days */
    @ConfigField(
        displayName = "Log Retention (Days)",
        description = "CloudWatch Logs retention period in days",
        category = "monitoring",
        allowedValues = {"1", "3", "5", "7", "14", "30", "60", "90", "120", "150", "180", "365", "400", "545", "731", "1096", "1827", "2192", "2557", "2922", "3288", "3653"},
        order = 10
    )
    public String logRetentionDays = null;

    // ========== Monitoring Configuration ==========

    /** Enable CloudWatch monitoring */
    @ConfigField(
        displayName = "Enable Monitoring",
        description = "Enable CloudWatch metrics and alarms",
        category = "monitoring",
        order = 20
    )
    public Boolean enableMonitoring = true;

    /** Enable encryption at rest */
    @ConfigField(
        displayName = "Enable Encryption",
        description = "Enable encryption at rest for all resources",
        category = "security",
        order = 320
    )
    public Boolean enableEncryption = true;

    /** Enable AWS Config */
    @ConfigField(
        displayName = "Enable AWS Config",
        description = "Enable AWS Config for configuration compliance monitoring",
        category = "compliance",
        order = 30
    )
    public Boolean awsConfigEnabled = false;

    /** Create AWS Config infrastructure — an account+region-wide singleton (AWS allows exactly
     *  one recorder and one delivery channel per account per region), so this must stay an
     *  explicit, single-stack-only opt-in. {@code false} by default, same as {@link
     *  #awsConfigEnabled}: unset means neither compliance rules nor the recorder/channel they'd
     *  attach to ever get created. */
    @ConfigField(
        displayName = "Create Config Infrastructure",
        description = "Create AWS Config recorder and delivery channel (only one per region — "
            + "only ever set this true on exactly one stack per account/region)",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true",
        order = 40
    )
    public Boolean createConfigInfrastructure = false;

    /** Scope AWS Config rules to only monitor this deployment's own resources (by stack name)
     *  instead of every resource in the account/region. */
    @ConfigField(
        displayName = "Scope Config Rules To Deployment",
        description = "Limit AWS Config rule monitoring to this deployment's own resources",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true",
        order = 45
    )
    public Boolean scopeConfigRulesToDeployment = false;

    /** Enable GuardDuty threat detection.
     *  Deliberately no static default -- see {@link #macieEnabled}. isGuardDutyEnabled() falls
     *  back to "always enabled for production" when unset; a {@code false} initializer here
     *  would make every deployment look like an explicit opt-out, permanently skipping both that
     *  fallback and the compliance-matrix requirement check ahead of it. */
    @ConfigField(
        displayName = "Enable GuardDuty",
        description = "Enable Amazon GuardDuty for threat detection",
        category = "compliance",
        order = 50
    )
    public Boolean guardDutyEnabled;

    /** Create GuardDuty detector (account-region singleton).
     *  Deliberately no static default -- see {@link #guardDutyEnabled} above; GuardDutyFactory
     *  only auto-sets this to true when it's unset. */
    @ConfigField(
        displayName = "Create GuardDuty Detector",
        description = "Create GuardDuty detector (only one per account/region)",
        category = "compliance",
        visibleWhen = "guardDutyEnabled == true",
        order = 60
    )
    public Boolean createGuardDutyDetector;

    /** GuardDuty alerts configured (EventBridge to SNS/SIEM) */
    @ConfigField(
        displayName = "Configure GuardDuty Alerts",
        description = "Configure EventBridge rules to forward GuardDuty findings to SNS/SIEM",
        category = "compliance",
        visibleWhen = "guardDutyEnabled == true",
        order = 65
    )
    public Boolean guardDutyAlertsConfigured = false;

    /** Certificate expiration monitoring enabled */
    @ConfigField(
        displayName = "Certificate Expiration Monitoring",
        description = "Enable CloudWatch alarms for ACM certificate expiration",
        category = "monitoring",
        visibleWhen = "enableSsl == true",
        order = 68
    )
    public Boolean certificateExpirationMonitoring = false;

    /** Enable CloudTrail for API audit logging */
    @ConfigField(
        displayName = "Enable CloudTrail",
        description = "Enable CloudTrail for API audit logging",
        category = "compliance",
        order = 70
    )
    public Boolean cloudTrailEnabled = false;

    /** Enable security monitoring */
    @ConfigField(
        displayName = "Security Monitoring",
        description = "Enable security monitoring features",
        category = "compliance",
        order = 72
    )
    public Boolean securityMonitoringEnabled = false;

    /** Enable EFS encryption in transit */
    @ConfigField(
        displayName = "EFS Encryption in Transit",
        description = "Enable EFS encryption in transit for data protection",
        category = "security",
        order = 340
    )
    public Boolean efsEncryptionInTransitEnabled = true;

    /** Restrict security group egress to VPC CIDR only (requires VPC endpoints for AWS service access) */
    @ConfigField(
        displayName = "Restrict Security Group Egress",
        description = "Restrict security group egress to VPC CIDR only. Requires VPC endpoints for AWS services (CloudWatch, RDS monitoring). If false, allows 0.0.0.0/0 egress (default CDK behavior).",
        category = "security",
        order = 345
    )
    public Boolean restrictSecurityGroupEgress = false;

    /** Enable automated backups (null = use security profile default) */
    @ConfigField(
        displayName = "Automated Backups",
        description = "Enable automated backups for EFS and databases (null = profile default: PRODUCTION=true, others=false)",
        category = "storage",
        order = 50
    )
    public Boolean automatedBackupEnabled = null;

    /** Enable cross-region backups (null = use security profile default) */
    @ConfigField(
        displayName = "Cross-Region Backups",
        description = "Enable cross-region backup replication for disaster recovery (null = profile default)",
        category = "storage",
        tags = {FieldTag.BILLING_IMPACT},
        order = 60
    )
    public Boolean crossRegionBackupEnabled = null;

    /** ARN of the AWS Backup vault, in another region, that backups are copied to. */
    @ConfigField(
        displayName = "Cross-Region Backup Vault ARN",
        description = "Destination backup vault for cross-region copies; no copy is made when unset",
        category = "storage",
        visibleWhen = "crossRegionBackupEnabled",
        order = 65
    )
    public String backupCrossRegionVaultArn;

    // ========== Advanced Monitoring & Threat Protection ==========

    /** Enable Amazon Macie for PII/PHI discovery (HIPAA/GDPR).
     *  Deliberately no static default -- see {@link #crossRegionBackupEnabled}. Left unset,
     *  isMacieEnabled() falls through to the compliance-matrix requirement and then the
     *  profile default; a {@code false} initializer here would make every deployment look like
     *  an explicit opt-out, permanently skipping both. */
    @ConfigField(
        displayName = "Enable Macie",
        description = "Enable Amazon Macie for PII/PHI discovery (required for HIPAA/GDPR)",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 80
    )
    public Boolean macieEnabled;

    /** Enable Macie automated discovery jobs */
    @ConfigField(
        displayName = "Macie Auto-Discovery",
        description = "Enable Macie automated discovery jobs for continuous scanning",
        category = "compliance",
        visibleWhen = "macieEnabled == true",
        dependsOn = "macieEnabled",
        tags = {FieldTag.BILLING_IMPACT},
        order = 90
    )
    // Deliberately no static default -- see macieEnabled above; isMacieAutomatedDiscoveryEnabled()
    // falls back to isMacieEnabled() when unset.
    public Boolean macieAutomatedDiscovery;

    /** Enable AWS Security Hub for centralized security findings.
     *  Deliberately no static default -- see {@link #macieEnabled} above. */
    @ConfigField(
        displayName = "Enable Security Hub",
        description = "Enable AWS Security Hub for centralized security findings",
        category = "compliance",
        order = 100
    )
    public Boolean securityHubEnabled;

    /** Enable the CIS AWS Foundations Benchmark standard in Security Hub */
    @ConfigField(
        displayName = "Security Hub: CIS Benchmark",
        description = "Enable the CIS AWS Foundations Benchmark standard",
        category = "compliance",
        visibleWhen = "securityHubEnabled == true",
        dependsOn = "securityHubEnabled",
        order = 101
    )
    public Boolean securityHubCisEnabled = false;

    /**
     * CIS AWS Foundations Benchmark version to enable. AWS has shipped 1.2.0, 1.4.0, 3.0.0,
     * and 5.0.0 as coexisting standard versions over time -- configurable so a future version
     * bump is a config change, not a code change.
     */
    @ConfigField(
        displayName = "Security Hub: CIS Benchmark Version",
        description = "CIS AWS Foundations Benchmark version to enable",
        category = "compliance",
        visibleWhen = "securityHubCisEnabled == true",
        dependsOn = "securityHubCisEnabled",
        example = "5.0.0",
        order = 101
    )
    public String securityHubCisVersion = "5.0.0";

    /** Enable the AWS Foundational Security Best Practices standard in Security Hub */
    @ConfigField(
        displayName = "Security Hub: AWS Foundational Security Best Practices",
        description = "Enable the AWS Foundational Security Best Practices standard",
        category = "compliance",
        visibleWhen = "securityHubEnabled == true",
        dependsOn = "securityHubEnabled",
        order = 102
    )
    public Boolean securityHubAwsFoundationalEnabled = false;

    /**
     * AWS Foundational Security Best Practices version to enable. Has stayed at 1.0.0 since
     * the standard launched in 2020 (controls are added within the version, not by version
     * bump), but configurable for consistency with the other two standards.
     */
    @ConfigField(
        displayName = "Security Hub: FSBP Version",
        description = "AWS Foundational Security Best Practices version to enable",
        category = "compliance",
        visibleWhen = "securityHubAwsFoundationalEnabled == true",
        dependsOn = "securityHubAwsFoundationalEnabled",
        example = "1.0.0",
        order = 103
    )
    public String securityHubFsbpVersion = "1.0.0";

    /** Enable the PCI DSS standard in Security Hub */
    @ConfigField(
        displayName = "Security Hub: PCI DSS",
        description = "Enable the PCI DSS standard",
        category = "compliance",
        visibleWhen = "securityHubEnabled == true",
        dependsOn = "securityHubEnabled",
        order = 104
    )
    public Boolean securityHubPciDssEnabled = false;

    /**
     * PCI DSS version to enable. 4.0.1 is current -- the PCI Council retired the prior
     * version, and AWS Security Hub follows suit. Configurable for the next version bump.
     */
    @ConfigField(
        displayName = "Security Hub: PCI DSS Version",
        description = "PCI DSS version to enable",
        category = "compliance",
        visibleWhen = "securityHubPciDssEnabled == true",
        dependsOn = "securityHubPciDssEnabled",
        example = "4.0.1",
        order = 105
    )
    public String securityHubPciDssVersion = "4.0.1";

    /** Enable Amazon Inspector for vulnerability scanning.
     *  Deliberately no static default -- see {@link #macieEnabled} above. */
    @ConfigField(
        displayName = "Enable Inspector",
        description = "Enable Amazon Inspector for vulnerability scanning",
        category = "compliance",
        order = 110
    )
    public Boolean inspectorEnabled;

    /** Enable Inspector EC2 instance scanning */
    @ConfigField(
        displayName = "Inspector: EC2 Scanning",
        description = "Enable Inspector vulnerability scanning for EC2 instances",
        category = "compliance",
        visibleWhen = "inspectorEnabled == true",
        dependsOn = "inspectorEnabled",
        order = 111
    )
    public Boolean inspectorEc2Scanning = true;

    /** Enable Inspector ECR image scanning */
    @ConfigField(
        displayName = "Inspector: ECR Scanning",
        description = "Enable Inspector vulnerability scanning for ECR container images",
        category = "compliance",
        visibleWhen = "inspectorEnabled == true",
        dependsOn = "inspectorEnabled",
        order = 112
    )
    public Boolean inspectorEcrScanning = true;

    /** Enable anti-malware scanning */
    @ConfigField(
        displayName = "Anti-Malware Scanning",
        description = "Enable anti-malware scanning for uploaded files",
        category = "compliance",
        order = 120
    )
    public Boolean antiMalwareEnabled = false;

    /** Enable file integrity monitoring */
    @ConfigField(
        displayName = "File Integrity Monitoring",
        description = "Monitor critical files for unauthorized changes",
        category = "compliance",
        order = 130
    )
    public Boolean fileIntegrityMonitoring = false;

    /** Enable container runtime security monitoring */
    @ConfigField(
        displayName = "Container Runtime Security",
        description = "Monitor container runtime for suspicious activity",
        category = "compliance",
        order = 140
    )
    public Boolean containerRuntimeSecurity = false;

    /** Enable container image vulnerability scanning */
    @ConfigField(
        displayName = "Container Image Scanning",
        description = "Scan container images for known vulnerabilities",
        category = "compliance",
        order = 150
    )
    public Boolean containerImageScanning = false;

    /** Enable AWS Audit Manager */
    @ConfigField(
        displayName = "Enable Audit Manager",
        description = "Enable AWS Audit Manager for compliance evidence collection",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 160
    )
    public Boolean auditManagerEnabled = false;

    /**
     * Enable the AWS Audit Manager service itself (null = use security profile default).
     *
     * <p>This is distinct from {@link #auditManagerEnabled}, which is the master gate
     * {@code SecurityRules.install()} uses to decide whether any CloudForge FrameworkRules
     * compliance validation runs at all. This field is the independent, configurable fact of
     * whether the Audit Manager service should actually be provisioned/enabled for a given
     * deployment -- it is what {@code SecurityProfileConfiguration#isAuditManagerEnabled()}
     * reads.</p>
     */
    @ConfigField(
        displayName = "Enable Audit Manager Service",
        description = "Provision the AWS Audit Manager service for this deployment (null = profile default: PRODUCTION=true, others=false)",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 165
    )
    public Boolean auditManagerServiceEnabled = null;

    /** Enable CloudWatch Logs KMS encryption */
    @ConfigField(
        displayName = "CloudWatch Logs KMS Encryption",
        description = "Encrypt CloudWatch Logs with customer-managed KMS keys (required for PCI-DSS, HIPAA, SOC2)",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 170
    )
    public Boolean cloudWatchLogsKmsEncryptionEnabled = false;

    /** Enable CloudTrail Insights */
    @ConfigField(
        displayName = "CloudTrail Insights",
        description = "Enable CloudTrail Insights for API activity anomaly detection (required for SOC2, NIST)",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        visibleWhen = "cloudTrailEnabled == true",
        dependsOn = "cloudTrailEnabled",
        order = 180
    )
    public Boolean cloudTrailInsightsEnabled = false;

    /** Enable Route53 Query Logging */
    @ConfigField(
        displayName = "Route53 Query Logging",
        description = "Enable DNS query logging for Route53 hosted zones (required for SOC2, NIST)",
        category = "compliance",
        order = 190
    )
    public Boolean route53QueryLoggingEnabled = false;

    /** Enable S3 Object Lock for audit buckets (HIPAA/PCI-DSS immutability requirement) */
    @ConfigField(
        displayName = "S3 Object Lock",
        description = "Enable S3 Object Lock for compliance audit buckets to ensure immutability (HIPAA § 164.312(c)(1), PCI-DSS Req 10.7)",
        category = "compliance",
        order = 195
    )
    public Boolean s3ObjectLockEnabled = false;

    /** Enable EBS volume encryption (null = profile default: PRODUCTION/STAGING=true unless the
     *  compliance matrix already requires it, DEV=true). Independent of {@link #efsEncryptionAtRestEnabled}
     *  and {@link #s3EncryptionEnabled} -- every framework's Rules.java reads these as 3 separate controls. */
    @ConfigField(
        displayName = "EBS Encryption",
        description = "Encrypt EBS volumes at rest (required for PCI-DSS, HIPAA, SOC2, GDPR)",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 196
    )
    public Boolean ebsEncryptionEnabled = null;

    /** Enable EFS at-rest encryption (null = profile default). See {@link #ebsEncryptionEnabled}. */
    @ConfigField(
        displayName = "EFS Encryption at Rest",
        description = "Encrypt EFS file systems at rest (required for PCI-DSS, HIPAA, SOC2, GDPR)",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 197
    )
    public Boolean efsEncryptionAtRestEnabled = null;

    /** Enable S3 bucket encryption (null = profile default). See {@link #ebsEncryptionEnabled}. */
    @ConfigField(
        displayName = "S3 Encryption",
        description = "Encrypt S3 buckets at rest (required for PCI-DSS, HIPAA, SOC2, GDPR)",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 198
    )
    public Boolean s3EncryptionEnabled = null;

    /** Enable AWS Backup Vault Lock (null = profile default: false except when the compliance
     *  matrix requires BACKUP_RECOVERY). Vault Lock makes the vault's retention policy immutable. */
    @ConfigField(
        displayName = "Backup Vault Lock",
        description = "Lock the AWS Backup vault's retention policy (required for PCI-DSS, HIPAA when enforced)",
        category = "compliance",
        order = 199
    )
    public Boolean backupVaultLockEnabled = null;

    /** Retain the AWS Backup vault on stack deletion (null = profile default). */
    @ConfigField(
        displayName = "Backup Vault Retention",
        description = "Retain the AWS Backup vault when the stack is deleted",
        category = "compliance",
        order = 200
    )
    public Boolean backupVaultRetentionEnabled = null;

    /** Enable RDS deletion protection (null = profile default: PRODUCTION=false unless the
     *  compliance matrix requires DELETION_PROTECTION, STAGING/DEV=false). */
    @ConfigField(
        displayName = "RDS Deletion Protection",
        description = "Block accidental deletion of the RDS instance",
        category = "compliance",
        order = 201
    )
    public Boolean rdsDeletionProtectionEnabled = null;

    /** Enable RDS Multi-AZ (null = profile default: PRODUCTION=true, STAGING/DEV=false unless the
     *  compliance matrix requires DATABASE_MULTI_AZ). */
    @ConfigField(
        displayName = "RDS Multi-AZ",
        description = "Deploy the RDS instance across multiple availability zones",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 202
    )
    public Boolean rdsDatabaseMultiAzEnabled = null;

    /** Enable SNS topic KMS encryption (null = profile default: false unless the compliance
     *  matrix requires SNS_KMS_ENCRYPTION). */
    @ConfigField(
        displayName = "SNS KMS Encryption",
        description = "Encrypt SNS topics with a customer-managed KMS key",
        category = "compliance",
        tags = {FieldTag.BILLING_IMPACT},
        order = 203
    )
    public Boolean snsKmsEncryptionEnabled = null;

    /** Require IMDSv2 on EC2 instances (null = profile default: PRODUCTION/STAGING=true unless the
     *  compliance matrix requires EC2_IMDSV2, DEV=false). ADVISORY for PCI-DSS and GDPR -- this
     *  field is what lets a deployer opt out where the framework doesn't mandate it. */
    @ConfigField(
        displayName = "IMDSv2 Required",
        description = "Require EC2 Instance Metadata Service Version 2 (blocks the legacy v1 endpoint)",
        category = "compliance",
        order = 204
    )
    public Boolean imdsv2Required = null;

    /** Enable S3 versioning remediation */
    @ConfigField(
        displayName = "S3 Versioning Remediation",
        description = "Enable automatic remediation of S3 versioning compliance violations",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true",
        order = 420
    )
    public Boolean enableS3VersioningRemediation = false;

    /** Enable CloudTrail bucket access logging remediation */
    @ConfigField(
        displayName = "CloudTrail Bucket Access Logging Remediation",
        description = "Enable automatic remediation of CloudTrail S3 bucket access logging violations",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true && cloudTrailEnabled == true",
        order = 430
    )
    public Boolean enableCloudTrailBucketAccessRemediation = false;

    /** Enable GuardDuty finding remediation (null = profile default: auto-enabled in PRODUCTION,
     *  same pattern as {@link #imdsv2Required} -- ComplianceFactory's consumers treat {@code null}
     *  as "PRODUCTION enables it, otherwise off" and an explicit value as an override either way,
     *  so this must NOT get a static default or that fallback can never trigger. */
    @ConfigField(
        displayName = "GuardDuty Remediation",
        description = "Enable automatic remediation of GuardDuty findings",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true",
        order = 440
    )
    public Boolean enableGuardDutyRemediation;

    /** Enable Security Hub finding remediation. Deliberately no static default -- see {@link
     *  #enableGuardDutyRemediation}. */
    @ConfigField(
        displayName = "Security Hub Remediation",
        description = "Enable automatic remediation of Security Hub findings",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true",
        order = 441
    )
    public Boolean enableSecurityHubRemediation;

    /** Enable Inspector finding remediation. Deliberately no static default -- see {@link
     *  #enableGuardDutyRemediation}. */
    @ConfigField(
        displayName = "Inspector Remediation",
        description = "Enable automatic remediation of Amazon Inspector findings",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true",
        order = 442
    )
    public Boolean enableInspectorRemediation;

    /** Enable Macie finding remediation. Deliberately no static default -- see {@link
     *  #enableGuardDutyRemediation}. */
    @ConfigField(
        displayName = "Macie Remediation",
        description = "Enable automatic remediation of Amazon Macie findings",
        category = "compliance",
        visibleWhen = "awsConfigEnabled == true",
        order = 443
    )
    public Boolean enableMacieRemediation;

    // ========== Health Check Configuration ==========

    @ConfigField(
        displayName = "Health Check Grace Period (seconds)",
        description = "Time to wait before starting health checks after container starts. GitLab needs 600s due to database migrations.",
        category = "resources",
        min = 60,
        max = 900,
        defaultFrom = "defaultHealthCheckGracePeriod",
        order = 600
    )
    public int healthCheckGracePeriod = 300;

    @ConfigField(
        displayName = "Health Check Interval (seconds)",
        description = "Time between health checks",
        category = "resources",
        min = 5,
        max = 300,
        order = 610
    )
    public int healthCheckInterval = 30;

    @ConfigField(
        displayName = "Health Check Timeout (seconds)",
        description = "Time to wait for health check response",
        category = "resources",
        min = 2,
        max = 60,
        order = 620
    )
    public int healthCheckTimeout = 5;

    @ConfigField(
        displayName = "Healthy Threshold Count",
        description = "Number of consecutive successful health checks before marking healthy",
        category = "resources",
        min = 1,
        max = 10,
        order = 630
    )
    public int healthyThreshold = 2;

    @ConfigField(
        displayName = "Unhealthy Threshold Count",
        description = "Number of consecutive failed health checks before marking unhealthy",
        category = "resources",
        min = 1,
        max = 10,
        order = 640
    )
    public int unhealthyThreshold = 3;

    // ========== Region Configuration ==========

    /** AWS region (e.g., "us-east-1", "us-west-2") */
    @ConfigField(
        displayName = "AWS Region",
        description = "AWS region for deployment",
        category = "basic",
        allowedValues = {
            "us-east-1", "us-east-2", "us-west-1", "us-west-2",
            "ca-central-1", "sa-east-1",
            "eu-west-1", "eu-west-2", "eu-west-3", "eu-central-1", "eu-north-1",
            "ap-south-1", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3",
            "ap-southeast-1", "ap-southeast-2"
        },
        required = true,
        order = 50
    )
    public String region = "us-east-1";

    /**
     * Target AWS account id to pin CDK's {@code Environment.account} to at synth time.
     *
     * <p>Deliberately <b>not</b> a {@code @ConfigField} — {@link
     * com.cloudforge.core.config.ConfigurationIntrospector} and {@link
     * com.cloudforge.core.config.DeploymentContextPreparer} both skip unannotated fields, so this
     * never appears in the generated deployment-context form/schema and never gets a default
     * resolved for it. It exists purely for callers that already know the target account
     * programmatically (cross-account deploy: CloudForge Manager resolves this from an assumed
     * IAM role before synthesis — see {@code cloudforge-manager}'s account-connection feature,
     * which owns every concept of "which account" beyond this bare pass-through field).</p>
     *
     * <p>When {@code null} (the default), {@code cloudforge-api}'s {@code CloudForgeSynthesizer}
     * falls back to {@code CDK_DEFAULT_ACCOUNT} if set, or omits the account, producing an
     * account-agnostic template.</p>
     */
    public String account;

    /**
     * GDPR data transfer approval flag for non-EU deployments.
     */
    @ConfigField(
        displayName = "GDPR Data Transfer Approved",
        description = "Confirm proper data transfer mechanisms (SCCs, BCRs) are in place for non-EU deployments with GDPR",
        category = "compliance",
        visibleWhen = "complianceFrameworks contains gdpr",
        dependsOn = "complianceFrameworks",
        order = 330
    )
    public Boolean gdprDataTransferApproved = false;

    /**
     * Availability zone suffixes for deployment (region-relative — "a" means whichever zone the
     * deploy's own region calls "a", not a specific full zone name, since the same suffix set is
     * valid regardless of which region gets picked elsewhere in the form).
     */
    @ConfigField(
        displayName = "Availability Zones",
        description = "Availability zone suffixes to deploy across (leave empty for automatic selection)",
        category = "network",
        allowedValues = {"a", "b", "c", "d"},
        example = "a,b",
        order = 5
    )
    public String[] availabilityZones;

    /** Enable auto-scaling */
    @ConfigField(
        displayName = "Enable Auto-Scaling",
        description = "Enable automatic scaling based on CPU utilization",
        category = "resources",
        order = 5
    )
    public Boolean enableAutoScaling = false;

    // ========== JSON Serialization/Deserialization ==========

    /**
     * Creates a pre-configured ObjectMapper for DeploymentConfig serialization.
     *
     * <p>Configuration:
     * <ul>
     *   <li>Field visibility (not getters) for public field serialization</li>
     *   <li>Enums serialized as strings</li>
     *   <li>Unknown properties ignored for forward compatibility</li>
     *   <li>Null values excluded from output</li>
     * </ul>
     */
    private static ObjectMapper createMapper() {
        ObjectMapper mapper = JsonMapper.builder()
                .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
                .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
                .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                // serializationInclusion(Include) is deprecated since Jackson 2.20 in favor of
                // defaultPropertyInclusion(Value); ALL_NON_NULL is the equivalent Value constant.
                .defaultPropertyInclusion(JsonInclude.Value.ALL_NON_NULL)
                .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
                .visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
                .build();
        return mapper;
    }

    /**
     * Load DeploymentConfig from a JSON file (e.g., deployment-context.json).
     *
     * @param path Path to the JSON file
     * @return DeploymentConfig populated from JSON
     * @throws IOException if file cannot be read or parsed
     */
    public static DeploymentConfig fromFile(Path path) throws IOException {
        return createMapper().readValue(path.toFile(), DeploymentConfig.class);
    }

    /**
     * Load DeploymentConfig from a JSON file path string.
     *
     * @param filePath Path to the JSON file
     * @return DeploymentConfig populated from JSON
     * @throws IOException if file cannot be read or parsed
     */
    public static DeploymentConfig fromFile(String filePath) throws IOException {
        return fromFile(Path.of(filePath));
    }

    /**
     * Load DeploymentConfig from a JSON string.
     *
     * @param json JSON string
     * @return DeploymentConfig populated from JSON
     * @throws JsonProcessingException if JSON cannot be parsed
     */
    public static DeploymentConfig fromJson(String json) throws JsonProcessingException {
        return createMapper().readValue(json, DeploymentConfig.class);
    }

    /**
     * Load DeploymentConfig from a Map (e.g., CDK context).
     *
     * <p>Uses Jackson's type-safe conversion to handle:
     * <ul>
     *   <li>String → Enum conversion via @JsonCreator methods</li>
     *   <li>String/Number → Boolean conversion (supports "1", "yes", "0", "no")</li>
     *   <li>Comma-separated strings → List conversion (complianceFrameworks)</li>
     *   <li>Unknown properties are ignored for forward compatibility</li>
     * </ul>
     *
     * @param map Map containing configuration key-value pairs
     * @return DeploymentConfig populated from the map
     */
    public static DeploymentConfig fromMap(java.util.Map<String, Object> map) {
        if (map == null || map.isEmpty()) {
            return new DeploymentConfig();
        }

        // Normalize the map: handle CDK context key rename and boolean string coercion
        java.util.Map<String, Object> normalized = new java.util.LinkedHashMap<>(map);
        if (normalized.containsKey("env") && !normalized.containsKey("environment")) {
            normalized.put("environment", normalized.get("env"));
        }

        // Coerce boolean-like string values before Jackson processing
        normalized.replaceAll((key, value) -> coerceBooleanIfNeeded(value));

        return createMapper().convertValue(normalized, DeploymentConfig.class);
    }

    /**
     * Coerces common boolean-like string values to actual Boolean objects.
     * Supports: "yes", "on" → true; "no", "off" → false.
     *
     * <p>Note: "1" and "0" are NOT coerced to boolean because they could be valid
     * integer values (e.g., minInstanceCapacity="1"). Jackson handles "true"/"false"
     * natively, so we only need to handle non-standard boolean representations.</p>
     */
    private static Object coerceBooleanIfNeeded(Object value) {
        if (value instanceof String str) {
            String lower = str.trim().toLowerCase();
            return switch (lower) {
                // Note: "1" and "0" removed - they're ambiguous with integer values
                case "yes", "on" -> Boolean.TRUE;
                case "no", "off" -> Boolean.FALSE;
                default -> value; // Keep original string for Jackson to parse
            };
        }
        return value;
    }

    /**
     * Serialize this DeploymentConfig to a JSON string.
     *
     * @return JSON string representation
     * @throws JsonProcessingException if serialization fails
     */
    public String toJson() throws JsonProcessingException {
        return createMapper().writerWithDefaultPrettyPrinter().writeValueAsString(this);
    }

    /**
     * Save this DeploymentConfig to a JSON file.
     *
     * @param path Path to write the JSON file
     * @throws IOException if file cannot be written
     */
    public void toFile(Path path) throws IOException {
        createMapper().writerWithDefaultPrettyPrinter().writeValue(path.toFile(), this);
    }

    /**
     * Save this DeploymentConfig to a JSON file path string.
     *
     * @param filePath Path to write the JSON file
     * @throws IOException if file cannot be written
     */
    public void toFile(String filePath) throws IOException {
        toFile(Path.of(filePath));
    }

    // ========== Compliance Framework Helpers ==========

    /**
     * Returns the compliance frameworks as a comma-separated string.
     * Provided for backward compatibility with code expecting the old string format.
     *
     * @return comma-separated framework string (e.g., "soc2,pci-dss,hipaa")
     */
    public String getComplianceFrameworksAsString() {
        return ComplianceFrameworkType.toCommaSeparated(complianceFrameworks);
    }

    /**
     * Checks if a specific compliance framework is enabled.
     *
     * @param framework the framework to check
     * @return true if the framework is in the list
     */
    public boolean hasComplianceFramework(ComplianceFrameworkType framework) {
        return complianceFrameworks != null && complianceFrameworks.contains(framework);
    }

    /**
     * Checks if any compliance framework is enabled.
     *
     * @return true if at least one framework is configured
     */
    public boolean hasAnyComplianceFramework() {
        return complianceFrameworks != null && !complianceFrameworks.isEmpty();
    }

    // ========== CloudForge Manager ==========

    /**
     * Opt-in: grants CloudForge Manager's own task/instance role the direct-deploy IAM
     * capability ({@code CFN_DEPLOY} — see {@code ManagerAwsCapabilityCatalog}),
     * condition-scoped to CloudForge-tagged resources.
     * Only has any effect when this deployment's {@code applicationId} is
     * {@code cloudforge-manager}; a no-op for every other application. Defaults to false — Manager
     * deploying AWS infrastructure on a caller's behalf (via {@code deploy:create}) is a
     * materially broader permission grant than Manager's normal inventory/operator role, so it
     * must be explicitly requested per deployment, not inherited automatically from IAM profile.
     */
    @ConfigField(
        displayName = "Manager Direct Deploy",
        description = "Grant CloudForge Manager's task role permission to create/update AWS "
            + "infrastructure directly (CFN CreateStack/UpdateStack + Service Catalog "
            + "ProvisionProduct), scoped to CloudForge-tagged resources. Only applies when "
            + "applicationId is cloudforge-manager.",
        category = "operations",
        visibleWhen = "applicationId == \"cloudforge-manager\"",
        required = false,
        tags = {FieldTag.REQUIRES_APPROVAL, FieldTag.EXPERIMENTAL},
        propertyKey = "cfc.manager.direct-deploy-enabled",
        order = 9030
    )
    public Boolean managerDirectDeployEnabled = false;

    /**
     * Convert this DeploymentConfig to a Map for CDK context.
     *
     * <p>Special handling:
     * <ul>
     *   <li>Renames "environment" to "env" for CDK compatibility</li>
     *   <li>Excludes null values</li>
     * </ul>
     *
     * @return Map suitable for CDK App context
     */
    @SuppressWarnings("unchecked")
    public Map<String, Object> toContextMap() {
        ObjectMapper mapper = createMapper();
        Map<String, Object> context = mapper.convertValue(this, Map.class);

        // CDK uses "env" not "environment"
        if (context.containsKey("environment")) {
            context.put("env", context.remove("environment"));
        }

        return context;
    }

}