ApplicationSpec.java

package com.cloudforge.core.interfaces;

import com.cloudforge.core.annotation.ApplicationPlugin;

import java.util.List;

/**
 * Application specification interface defining application-specific configuration.
 * This enables CloudForge to deploy any application (Jenkins, GitLab, Vault, etc.)
 * using the same infrastructure patterns.
 *
 * <p>Implementations provide configuration for both container (Fargate) and
 * EC2 deployments, supporting both EFS and EBS storage strategies.</p>
 *
 * <p>CloudForge 3.0.0: Universal Application Support</p>
 *
 * <p>Example implementations:</p>
 * <ul>
 *   <li>JenkinsApplicationSpec: Jenkins CI/CD automation server</li>
 *   <li>GitLabApplicationSpec: GitLab DevOps platform</li>
 *   <li>GrafanaApplicationSpec: Grafana metrics visualization</li>
 *   <li>PostgreSQLApplicationSpec: PostgreSQL database</li>
 *   <li>VaultApplicationSpec: HashiCorp Vault secrets management</li>
 *   <li>+ 9 more built-in applications</li>
 * </ul>
 *
 * <h2>Plugin Metadata:</h2>
 * <p>Implementations should be annotated with {@link ApplicationPlugin} for auto-discovery
 * and metadata support:</p>
 * <pre>{@code
 * @ApplicationPlugin(
 *     value = "jenkins",
 *     category = "cicd",
 *     displayName = "Jenkins",
 *     description = "Open-source automation server for CI/CD"
 * )
 * public class JenkinsApplicationSpec implements ApplicationSpec {
 *     // ...
 * }
 * }</pre>
 */
public interface ApplicationSpec {

    // ========== Application Identity ==========

    /**
     * Returns a unique identifier for this application.
     * Used for logging, metrics, and resource naming.
     *
     * @return application identifier (e.g., "jenkins", "gitlab", "vault")
     */
    String applicationId();

    // ========== Container Configuration ==========

    /**
     * Returns the default container image for this application.
     * Can be overridden by deployment context configuration.
     *
     * @return container image string (e.g., "jenkins/jenkins:lts")
     */
    String defaultContainerImage();

    /**
     * Returns the primary application port.
     * This is the port the application listens on inside the container.
     *
     * @return application port (e.g., 8080 for Jenkins)
     */
    int applicationPort();

    /**
     * Returns the container path where application data is stored.
     * This is where the volume will be mounted inside the container.
     *
     * @return container mount path (e.g., "/var/jenkins_home")
     */
    String containerDataPath();

    /**
     * Returns the EFS path for this application's data.
     * This is the path within the EFS filesystem.
     *
     * @return EFS path (e.g., "/jenkins")
     */
    String efsDataPath();

    /**
     * Returns the volume name for this application.
     * Used to reference the volume in task definitions.
     *
     * @return volume name (e.g., "jenkinsHome")
     */
    String volumeName();

    /**
     * Returns the container user (UID:GID) to run as.
     * Important for file permissions when using EFS.
     *
     * @return user in format "UID:GID" (e.g., "1000:1000")
     */
    String containerUser();

    /**
     * Returns the EFS permissions for the access point.
     *
     * @return permissions string (e.g., "750")
     */
    String efsPermissions();

    /**
     * Returns the CPU architecture this application's container image is built for, so a Fargate
     * task definition requests matching compute rather than silently defaulting to X86_64.
     *
     * <p>Defaults to {@code X86_64} -- almost every built-in application image (Jenkins, GitLab,
     * Grafana, etc.) is x86_64-only or x86_64-primary today. Override only when the image is
     * confirmed to actually support the declared architecture; a mismatch fails the task at
     * startup with no fallback, since Fargate provisions the underlying compute from this value
     * before it ever pulls the image.</p>
     *
     * @return the target CPU architecture, defaulting to X86_64
     */
    default com.cloudforge.core.enums.CpuArchitecture cpuArchitecture() {
        return com.cloudforge.core.enums.CpuArchitecture.X86_64;
    }

    /**
     * Configures application-specific environment variables for the container.
     *
     * <p>Applications can override this to provide custom environment variables
     * based on deployment configuration (FQDN, SSL, authMode, etc.). The infrastructure
     * passes the FQDN, SSL settings, and authentication mode for applications that need
     * reverse proxy configuration or authentication-specific setup.</p>
     *
     * <p>Example use cases:</p>
     * <ul>
     *   <li>Jenkins: JAVA_OPTS, JENKINS_OPTS for reverse proxy configuration, skip setup wizard for application-oidc</li>
     *   <li>GitLab: GITLAB_OMNIBUS_CONFIG for external URL configuration and OIDC setup</li>
     *   <li>Vault: VAULT_ADDR for API endpoint configuration</li>
     * </ul>
     *
     * @param fqdn The fully qualified domain name (may be null)
     * @param sslEnabled Whether SSL is enabled
     * @param authMode The authentication mode (may be null, e.g., "none", "alb-oidc", "application-oidc")
     * @return Map of environment variable key-value pairs (never null, may be empty)
     */
    default java.util.Map<String, String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode) {
        return java.util.Collections.emptyMap();
    }

    /**
     * Returns the health check path for ALB/ELB health checks.
     *
     * <p>Different applications expose health endpoints at different paths:</p>
     * <ul>
     *   <li>Jenkins: /login</li>
     *   <li>GitLab: /users/sign_in</li>
     *   <li>Grafana: /api/health</li>
     *   <li>Metabase: /api/health</li>
     * </ul>
     *
     * @return health check path (e.g., "/login", "/api/health")
     */
    default String healthCheckPath() {
        return "/";
    }

    // ========== EC2 Configuration ==========

    /**
     * Returns the EBS device name for EC2 instances when not using EFS.
     * This is the device that will be formatted and mounted for application data.
     *
     * @return EBS device path (e.g., "/dev/xvdh")
     */
    String ebsDeviceName();

    /**
     * Returns the EC2 data path where application stores persistent data.
     * This may differ from containerDataPath depending on application packaging.
     *
     * @return EC2 mount path (e.g., "/var/lib/jenkins")
     */
    String ec2DataPath();

    /**
     * Returns CloudWatch log file paths for EC2 monitoring.
     * These files will be streamed to CloudWatch Logs for centralized logging.
     *
     * @return list of absolute log file paths (e.g., ["/var/log/jenkins/jenkins.log"])
     */
    List<String> ec2LogPaths();

    /**
     * Configure EC2 UserData script for application installation and setup.
     *
     * <p>The implementation should use the UserDataBuilder to add application-specific
     * installation commands while leveraging infrastructure helpers for storage mounting
     * and CloudWatch configuration.</p>
     *
     * <p>The infrastructure handles:</p>
     * <ul>
     *   <li>System updates</li>
     *   <li>EFS vs EBS storage mounting (based on availability)</li>
     *   <li>CloudWatch Agent installation and configuration</li>
     *   <li>File permissions and ownership</li>
     * </ul>
     *
     * <p>The application provides:</p>
     * <ul>
     *   <li>Application installation commands (yum/dnf install, etc.)</li>
     *   <li>Application configuration</li>
     *   <li>Service startup commands</li>
     * </ul>
     *
     * @param builder The UserDataBuilder providing infrastructure helpers
     * @param context The Ec2Context providing runtime information
     */
    void configureUserData(UserDataBuilder builder, Ec2Context context);

    // ========== OIDC Integration (Optional) ==========

    /**
     * Returns whether this application supports OIDC integration.
     *
     * <p>Applications with built-in OIDC support (GitLab, Grafana, SonarQube) or
     * plugin support (Jenkins) should return true.</p>
     *
     * @return true if application can integrate with OIDC providers
     */
    default boolean supportsOidcIntegration() {
        return false;
    }

    /**
     * Returns the OIDC integration handler for this application.
     *
     * <p>This provides application-specific configuration for integrating with
     * Cognito or IAM Identity Center OIDC.</p>
     *
     * @return OIDC integration handler, or null if not supported
     */
    default OidcIntegration getOidcIntegration() {
        return null;
    }

    /**
     * Returns the list of supported authentication modes for this application.
     *
     * <p>CloudForge supports three authentication modes:</p>
     * <ul>
     *   <li><b>application-oidc</b>: OIDC authentication integrated within the application (requires getOidcIntegration() != null)</li>
     *   <li><b>alb-oidc</b>: OIDC authentication at ALB level (works for all applications)</li>
     *   <li><b>none</b>: No authentication (public access or manually configured)</li>
     * </ul>
     *
     * <p>The list is ordered by preference. The first mode is the recommended default.</p>
     *
     * <p>Default behavior:</p>
     * <ul>
     *   <li>If application has OIDC integration → ["application-oidc", "alb-oidc", "none"]</li>
     *   <li>If application claims OIDC support but lacks integration → ["alb-oidc", "none"]</li>
     *   <li>If application doesn't support OIDC → ["none"]</li>
     * </ul>
     *
     * @return List of supported auth modes in order of preference (never null, never empty)
     */
    default List<String> getSupportedAuthModes() {
        if (getOidcIntegration() != null) {
            // Application has working OIDC integration - prefer application-oidc
            return List.of("application-oidc", "alb-oidc", "none");
        } else if (supportsOidcIntegration()) {
            // Application claims OIDC support but doesn't implement it - use alb-oidc
            return List.of("alb-oidc", "none");
        } else {
            // No OIDC support at all
            return List.of("none");
        }
    }

    /**
     * Auth modes allowed for a deployment target ({@code aws}, {@code ministack}, {@code localstack}).
     *
     * <p>Default: same as {@link #getSupportedAuthModes()}. Specs may restrict Cognito/OIDC
     * on MiniStack while keeping it optional on LocalStack/AWS.</p>
     *
     * @param deploymentTarget target id, or null/blank for the default list
     * @return non-empty list of auth mode strings
     */
    default List<String> getSupportedAuthModes(String deploymentTarget) {
        return getSupportedAuthModes();
    }

    /**
     * Returns the recommended (default) authentication mode for this application.
     *
     * <p>This is the first mode from {@link #getSupportedAuthModes()}.</p>
     *
     * @return the recommended auth mode (e.g., "application-oidc", "alb-oidc", "none")
     */
    default String getRecommendedAuthMode() {
        return getSupportedAuthModes().get(0);
    }

    /**
     * Recommended auth mode for a deployment target.
     *
     * @param deploymentTarget target id ({@code aws}, {@code ministack}, {@code localstack})
     * @return first entry from {@link #getSupportedAuthModes(String)}
     */
    default String getRecommendedAuthMode(String deploymentTarget) {
        return getSupportedAuthModes(deploymentTarget).get(0);
    }

    // ========== Path-Based Authentication ==========

    /**
     * Returns paths that require authentication when using ALB-level OIDC.
     *
     * <p>When this list is non-empty and authMode is "alb-oidc", the ALB will:
     * <ul>
     *   <li>Require OIDC authentication for requests matching these paths</li>
     *   <li>Allow unauthenticated access to all other paths</li>
     * </ul>
     *
     * <p>When this list is empty (default), ALL paths require authentication.</p>
     *
     * <p>Path patterns support ALB path-pattern syntax:</p>
     * <ul>
     *   <li>Exact: "/admin"</li>
     *   <li>Prefix wildcard: "/admin/*"</li>
     *   <li>Extension: "*.php"</li>
     * </ul>
     *
     * <p>Example for phpBB (protect admin and installer):</p>
     * <pre>{@code
     * @Override
     * public List<String> protectedPaths() {
     *     return List.of("/adm/*", "/install/*");
     * }
     * }</pre>
     *
     * <p>Example for WordPress (protect wp-admin):</p>
     * <pre>{@code
     * @Override
     * public List<String> protectedPaths() {
     *     return List.of("/wp-admin/*", "/wp-login.php");
     * }
     * }</pre>
     *
     * <p>Users can override these defaults via DeploymentContext:</p>
     * <ul>
     *   <li>protectedPaths: Override/replace the application defaults</li>
     *   <li>additionalProtectedPaths: Add to the application defaults</li>
     *   <li>publicPaths: Explicitly mark paths as public (overrides protected)</li>
     * </ul>
     *
     * @return list of path patterns requiring authentication (empty = protect everything)
     * @see #publicPaths()
     */
    default List<String> protectedPaths() {
        return List.of();  // Default: protect everything when auth is enabled
    }

    /**
     * Returns paths that should always be public (no authentication required).
     *
     * <p>These paths are excluded from authentication even when they would
     * otherwise be protected. Useful for health checks, public APIs, etc.</p>
     *
     * <p>Common use cases:</p>
     * <ul>
     *   <li>Health check endpoints: "/health", "/api/health"</li>
     *   <li>Public API endpoints: "/api/public/*"</li>
     *   <li>Static assets: "/static/*", "/assets/*"</li>
     * </ul>
     *
     * @return list of path patterns that should be public (empty by default)
     */
    default List<String> publicPaths() {
        return List.of();  // Default: no explicit public paths
    }

    // ========== Optional Ports (Security-Conscious) ==========

    /**
     * Optional service port that can be enabled via deployment configuration.
     *
     * <p>Ports are NOT exposed by default - must be explicitly enabled via the configKey
     * in deployment configuration. This follows the principle of least privilege.</p>
     *
     * @param port The port number
     * @param protocol The protocol ("tcp" or "udp")
     * @param configKey The DeploymentContext key to enable this port (e.g., "enableSmtp")
     * @param service Human-readable service name for logging/prompts
     * @param inbound true if port accepts inbound connections (requires security group rule),
     *                false if outbound only (container connects out, no SG rule needed)
     */
    record OptionalPort(int port, String protocol, String configKey, String service, boolean inbound) {
        /**
         * Convenience constructor for inbound TCP ports.
         *
         * @param port the port number
         * @param configKey the deployment config key to enable this port
         * @param service the service name using this port
         * @return an OptionalPort configured for inbound TCP
         */
        public static OptionalPort inboundTcp(int port, String configKey, String service) {
            return new OptionalPort(port, "tcp", configKey, service, true);
        }

        /**
         * Convenience constructor for outbound TCP ports (no security group rule needed).
         *
         * @param port the port number
         * @param configKey the deployment config key to enable this port
         * @param service the service name using this port
         * @return an OptionalPort configured for outbound TCP
         */
        public static OptionalPort outboundTcp(int port, String configKey, String service) {
            return new OptionalPort(port, "tcp", configKey, service, false);
        }
    }

    /**
     * Returns optional ports that can be enabled via deployment configuration.
     *
     * <p>These ports are NOT exposed by default. Users must set the corresponding
     * configKey to true in their deployment configuration to enable each port.</p>
     *
     * <p>Example implementation for Mattermost:</p>
     * <pre>{@code
     * @Override
     * public List<OptionalPort> optionalPorts() {
     *     return List.of(
     *         OptionalPort.outboundTcp(587, "enableSmtp", "SMTP Email"),
     *         OptionalPort.inboundTcp(8074, "enableClustering", "Cluster Gossip")
     *     );
     * }
     * }</pre>
     *
     * <p>User enables in deployment-context.json:</p>
     * <pre>{@code
     * {
     *   "enableSmtp": true,
     *   "enableClustering": true
     * }
     * }</pre>
     *
     * @return list of optional ports (empty by default - most apps only need primary port)
     */
    default List<OptionalPort> optionalPorts() {
        return List.of();
    }

    /**
     * Declares an always-running, same-Fargate-task companion container: {@code ContainerFactory}
     * adds it to the same task definition as this app's own container, wires the main container
     * to wait for it (a startup dependency on {@code HEALTHY}, not {@code SUCCESS} — unlike the
     * SAML certificate init container this deliberately isn't modeled after, a sidecar never
     * exits), and gives it its own CloudWatch log stream. No IAM task-role grants and no inbound
     * security-group rule are added for it — a sidecar reachable only over the task's own loopback
     * interface (the way every container in one Fargate/{@code awsvpc} task already shares one ENI)
     * needs neither.
     *
     * @param containerName unique name within the task definition (also the log stream prefix)
     * @param image container image reference (registry/repo:tag)
     * @param containerPort port the sidecar listens on inside the task's shared network namespace
     * @param healthCheckCommand ECS container health check command, e.g.
     *     {@code List.of("CMD-SHELL", "curl -f http://localhost:8090/v1/healthz || exit 1")} —
     *     required, since the main container's startup dependency needs a {@code HEALTHY} signal
     *     to wait on. The port in that example is the {@code cloudforge-synth-service} sidecar's
     *     own default — see {@link com.cloudforge.core.manager.ManagerEnvKeys#SYNTH_SERVICE_DEFAULT_PORT}
     *     for the one place that port is actually declared
     * @param environment environment variables for the sidecar container
     */
    record SidecarContainer(String containerName, String image, int containerPort,
                             List<String> healthCheckCommand, java.util.Map<String, String> environment) {
    }

    /**
     * Returns same-task sidecar containers this application needs alongside its own container.
     *
     * <p>Empty for every application by default — this exists for
     * {@code cloudforge-manager}'s {@code cloudforge-synth-service} sidecar (see that spec's own
     * override), not a general-purpose extension point most applications need.</p>
     *
     * @return list of sidecar containers (empty by default)
     */
    default List<SidecarContainer> sidecarContainers() {
        return List.of();
    }

    // ========== Plugin Metadata Methods ==========

    /**
     * Get the application category from the {@link ApplicationPlugin} annotation.
     *
     * @return the category (e.g., "cicd", "monitoring", "database")
     */
    default String category() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return "unknown";
        }
        return annotation.category();
    }

    /**
     * Get the human-readable display name for this application.
     *
     * @return the display name, defaulting to capitalized {@link #applicationId()} if not specified
     */
    default String displayName() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            String id = applicationId();
            return id.substring(0, 1).toUpperCase() + id.substring(1);
        }
        String displayName = annotation.displayName();
        if (displayName.isEmpty()) {
            String id = applicationId();
            return id.substring(0, 1).toUpperCase() + id.substring(1);
        }
        return displayName;
    }

    /**
     * Get the application description.
     *
     * @return the application description
     */
    default String description() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return "";
        }
        return annotation.description();
    }

    /**
     * Get the default Fargate CPU units.
     *
     * @return the default CPU units (256, 512, 1024, 2048, 4096)
     */
    default int defaultCpu() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return 1024; // Default 1 vCPU
        }
        return annotation.defaultCpu();
    }

    /**
     * Get the default Fargate memory in MB.
     *
     * @return the default memory in MB
     */
    default int defaultMemory() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return 2048; // Default 2GB
        }
        return annotation.defaultMemory();
    }

    /**
     * Get the default EC2 instance type.
     *
     * @return the default instance type (e.g., "t3.small")
     */
    default String defaultInstanceType() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return "t3.small";
        }
        return annotation.defaultInstanceType();
    }

    /**
     * Check if this application supports Fargate deployment.
     *
     * @return true if Fargate is supported
     */
    default boolean supportsFargate() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return true; // Default to supported
        }
        return annotation.supportsFargate();
    }

    /**
     * Check if this application supports EC2 deployment.
     *
     * @return true if EC2 is supported
     */
    default boolean supportsEc2() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return true; // Default to supported
        }
        return annotation.supportsEc2();
    }

    /**
     * Whether this application requires a managed database (RDS) in the canonical template.
     *
     * <p>Reads {@link ApplicationPlugin#requiresDatabase()} when present; defaults to false.</p>
     */
    default boolean requiresDatabase() {
        ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
        if (annotation == null) {
            return false;
        }
        return annotation.requiresDatabase();
    }

    /**
     * Get the recommended health check grace period for this application.
     *
     * <p>The grace period is how long ECS/ALB waits before starting health checks
     * after a container starts. Applications with longer initialization times
     * (like GitLab) need longer grace periods.</p>
     *
     * <p>Default values:</p>
     * <ul>
     *   <li>Most applications: 300 seconds (5 minutes)</li>
     *   <li>GitLab: 600 seconds (10 minutes) - due to database migrations and initialization</li>
     *   <li>Other database-heavy apps may also need longer periods</li>
     * </ul>
     *
     * @return recommended health check grace period in seconds
     */
    default int defaultHealthCheckGracePeriod() {
        return 300; // Default 5 minutes for most applications
    }

    /**
     * The container environment variable name a generated initial-admin-password secret should
     * be bound to, or {@code null} (the default) if this application has no such variable.
     *
     * <p>Mirrors {@code CmsSpec.databaseEnvVars}'s existing pattern for the database password
     * specifically: a database connection's host/port/name/user travel as plain strings through
     * {@link #containerEnvironmentVariables}, but the password itself is never a plaintext value
     * an app spec method returns — it's delivered as a Secrets Manager-backed ECS Secret, which
     * needs a CDK construct only {@code cloudforge-api} can build. This method is the same shape,
     * generalized to any application whose official image supports a fully non-interactive
     * first-run install once its site name/admin account/admin password are all present (Joomla's
     * own {@code docker-entrypoint.sh} is the first example — see {@code JoomlaApplicationSpec}),
     * instead of a per-application name check hardcoded into {@code ApplicationFactory}/
     * {@code ContainerFactory}.</p>
     *
     * @return the env var name (e.g. {@code "JOOMLA_ADMIN_PASSWORD"}), or {@code null}
     */
    default String autoAdminPasswordEnvVar() {
        return null;
    }

    /**
     * Whether this application needs a Redis-backed session store (ElastiCache), provisioned by
     * {@code ApplicationFactory} and delivered as {@code redisSessionStoreEndpoint}/
     * {@code redisSessionStorePort} in {@code SystemContext}. Same generalization shape as
     * {@link #autoAdminPasswordEnvVar()} — an application declares the need through this contract
     * rather than {@code ApplicationFactory} recognizing it by {@link #applicationId()}.
     *
     * @return true if a Redis session store should be provisioned
     */
    default boolean requiresSessionStore() {
        return false;
    }

    /**
     * The container environment variable name a generated AES cipher-key secret should be bound
     * to, or {@code null} (the default) if this application has no such need. Same shape as
     * {@link #autoAdminPasswordEnvVar()}: {@code ApplicationFactory} provisions the Secrets
     * Manager entry and {@code ContainerFactory} binds it as an ECS Secret under this name,
     * without either needing to recognize a specific {@link #applicationId()}.
     *
     * @return the env var name (e.g. {@code "CFC_MANAGER_ACCOUNT_SECRET_KEY"}), or {@code null}
     */
    default String cipherKeySecretEnvVar() {
        return null;
    }

    /**
     * The container environment variable name a deploy-time-supplied license key should be bound
     * to, or {@code null} (the default) if this application has no license key concept. Same
     * shape as {@link #cipherKeySecretEnvVar()} — the value itself still comes from whatever
     * deployment-context field the application's own deployment contract declares; this only
     * names the delivery env var.
     *
     * @return the env var name (e.g. {@code "CFC_MANAGER_LICENSESEAT_LICENSE_KEY"}), or {@code null}
     */
    default String licenseKeySecretEnvVar() {
        return null;
    }

    /**
     * Whether this application's persistence can't safely tolerate the brief two-task overlap a
     * normal zero-downtime rolling ECS deployment creates (new task starts before the old one
     * stops) — e.g. a single-writer embedded-file database with no real concurrent-access support.
     * Only takes effect when this application also has no managed database connection provisioned
     * (see {@code DatabaseSpec}) — once one is, a real database safely handles that overlap and
     * this no longer applies regardless of what this method returns.
     *
     * @return true if this application needs a stop-then-start deployment replacement instead of
     *     ECS's own rolling default, when running without a managed database
     */
    default boolean requiresSequentialDeploymentWithoutDatabase() {
        return false;
    }

    /**
     * The container environment variable name the ALB's own ARN should be bound to when this
     * application is deployed with {@code authMode=alb-oidc}, or {@code null} (the default) if
     * this application has no need to know it (e.g. to validate an ALB-signed OIDC token itself).
     *
     * @return the env var name, or {@code null}
     */
    default String albSignerArnEnvVar() {
        return null;
    }

    /**
     * The container environment variable name a "was this deployment's public endpoint reachable
     * over a publicly-trusted TLS certificate" boolean signal should be bound to, or {@code null}
     * (the default) if this application has no such need.
     *
     * @return the env var name, or {@code null}
     * @see com.cloudforge.core.interfaces.OidcIntegration
     */
    default String publicTlsTrustedEnvVar() {
        return null;
    }

    /**
     * The container environment variable name the resolved deployment target
     * ({@code aws}/{@code localstack}/{@code ministack}) should be bound to, or {@code null} (the
     * default) if this application has no need to distinguish targets at runtime.
     *
     * @return the env var name, or {@code null}
     */
    default String deploymentTargetEnvVar() {
        return null;
    }

    /**
     * The four container environment variable names a provisioned Redis session-store connection
     * should be bound to (mode/host/port/TLS-enabled, in that order), or {@code null} (the
     * default) if this application has no session-store concept — same gate
     * {@link #requiresSessionStore()} uses for whether to provision the cluster at all; this only
     * names where its connection details land once provisioned. The TLS-enabled var is always
     * told {@code "false"} — the cluster this provisions has no TLS listener.
     *
     * @return {@code [modeEnvVar, hostEnvVar, portEnvVar, tlsEnabledEnvVar]}, or {@code null}
     */
    default String[] sessionStoreEnvVars() {
        return null;
    }

    /**
     * The container environment variable name a provisioned database's password secret should be
     * bound to, overriding {@code ContainerFactory}'s own per-{@link #applicationId()} switch
     * (and its {@code DATABASE_PASSWORD} fallback for any application not in it) when non-null.
     * Most built-in applications are covered by that switch already and don't need this — it
     * exists for an application declared outside {@code cloudforge-api} entirely, which the
     * switch can't have a case for.
     *
     * @return the env var name, or {@code null} to use the switch/fallback instead
     */
    default String databasePasswordEnvVar() {
        return null;
    }

    /**
     * The container environment variable name an application-OIDC client secret should be bound
     * to, overriding {@code ContainerFactory}'s own per-{@link #applicationId()} switch (and its
     * {@code <APP>_OIDC_CLIENT_SECRET} fallback naming for any application not in it) when
     * non-null. Same reasoning as {@link #databasePasswordEnvVar()} — exists for an application
     * declared outside {@code cloudforge-api} entirely, whose expected env var name doesn't match
     * that generic {@code <APP>_} naming convention.
     *
     * @return the env var name, or {@code null} to use the switch/fallback instead
     */
    default String oidcClientSecretEnvVar() {
        return null;
    }

    /**
     * Whether this application writes a role edit through to its Cognito user pool's real group
     * membership at runtime (e.g. {@code AdminAddUserToGroup}) rather than only updating its own
     * local cache — {@code CognitoAuthenticationFactory} grants the corresponding IAM actions on
     * the pool's task role only when this is true, since most applications never call those APIs
     * at all and shouldn't carry the grant.
     *
     * @return true if the Cognito admin group-management IAM actions should be granted
     */
    default boolean requiresCognitoGroupManagementIam() {
        return false;
    }

    /**
     * Default admin/developer group names for an external-OIDC-provider deployment, used when the
     * deployment context doesn't override them ({@code cognitoAdminGroupName}/
     * {@code cognitoUserGroupName}).
     *
     * @return {@code [adminGroupName, developerGroupName]}, defaulting to {@code ["Admins",
     *     "Developers"]}
     */
    default String[] defaultOidcGroupNames() {
        return new String[] {"Admins", "Developers"};
    }
}