CmsSpec.java

package com.cloudforge.core.interfaces;

import com.cloudforge.core.annotation.CmsPlugin;

import java.util.List;
import java.util.Map;

/**
 * CMS/E-commerce specification interface extending ApplicationSpec.
 *
 * <p>Provides CMS-specific configuration for PHP-based content management
 * and e-commerce platforms including WordPress, Magento, Joomla, PrestaShop,
 * and Drupal.</p>
 *
 * <p>Example implementations:</p>
 * <ul>
 *   <li>WordPressApplicationSpec: PHP-based CMS and blogging platform</li>
 *   <li>WooCommerceApplicationSpec: WordPress-based e-commerce</li>
 *   <li>MagentoApplicationSpec: Adobe Commerce-compatible e-commerce platform</li>
 *   <li>JoomlaApplicationSpec: General-purpose CMS for websites</li>
 *   <li>PrestaShopApplicationSpec: Open-source e-commerce</li>
 *   <li>DrupalApplicationSpec: CMS with OIDC support</li>
 * </ul>
 *
 * <h2>Configuration</h2>
 * <ul>
 *   <li>PHP runtime configuration (version, extensions, OPcache, PHP-FPM)</li>
 *   <li>S3 media storage offloading</li>
 *   <li>CDN integration for static assets</li>
 *   <li>Redis/Memcached object caching</li>
 *   <li>Scheduled task management (system cron vs internal)</li>
 *   <li>Multi-site/multi-store support</li>
 *   <li>OIDC authentication via plugins</li>
 * </ul>
 *
 * <h2>4.0 migration intent</h2>
 * <p>This interface is the 3.x CMS/e-commerce convenience contract and remains supported as a
 * compatibility surface during the 4.0 migration. It must not become the template for a new
 * large interface per application family. A CMS implementation remains an {@link ApplicationSpec}
 * and will be adapted into the same typed application requirements, provider selection, policy,
 * entitlement, compliance, resolved-plan, and binding contracts used by every other application.</p>
 *
 * <p>The adapter maps shared infrastructure concerns to named requirements, for example:</p>
 * <ul>
 *   <li>PHP version, extensions, FPM and OPcache settings to a runtime requirement;</li>
 *   <li>the primary CMS database to {@code database.primary};</li>
 *   <li>persistent content/files and media offload to named storage requirements;</li>
 *   <li>object/session/page cache to named cache requirements;</li>
 *   <li>CDN, TLS, DNS, and public/admin routes to delivery and ingress policies; and</li>
 *   <li>cron/background work to scheduled-work requirements.</li>
 * </ul>
 *
 * <p>CMS-specific runtime behavior stays application-owned: a plugin may still describe PHP
 * configuration and how resolved database, storage, cache, identity, and ingress bindings become
 * its environment, secret references, configuration files, commands, or EC2 user data. The CMS
 * plugin does not select arbitrary infrastructure providers, create raw resources, override
 * platform profiles, bypass entitlement restrictions, or weaken required compliance controls.</p>
 *
 * <p>New 4.0 application plugins should declare the narrow reusable requirement records directly.
 * Existing {@code CmsSpec}, {@link CmsMediaStorageSpec}, and {@link CmsObjectCacheSpec}
 * implementations will be retained behind compatibility adapters until synthesis, integration,
 * parameterized, and compliance regressions prove equivalent behavior.</p>
 *
 * @since 3.1.0
 * @see ApplicationSpec
 * @see com.cloudforge.core.annotation.CmsPlugin
 */
public interface CmsSpec extends ApplicationSpec {

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

    /**
     * Returns the required PHP version for this CMS.
     *
     * <p>Common versions:</p>
     * <ul>
     *   <li>8.2 - WordPress, Magento 2.4.6+, Drupal 10</li>
     *   <li>8.1 - PrestaShop 8.x, Joomla 5</li>
     *   <li>7.4 - Legacy support (not recommended)</li>
     * </ul>
     *
     * @return PHP version string (e.g., "8.2", "8.1")
     */
    String phpVersion();

    /**
     * Returns required PHP extensions for this CMS.
     *
     * <p>Common extensions include:</p>
     * <ul>
     *   <li>mysqli, pdo_mysql - MySQL database connectivity</li>
     *   <li>gd, imagick - Image processing</li>
     *   <li>curl - HTTP client</li>
     *   <li>mbstring - Multibyte string handling</li>
     *   <li>xml, dom - XML processing</li>
     *   <li>zip - Archive handling</li>
     *   <li>intl - Internationalization</li>
     *   <li>opcache - Bytecode caching</li>
     *   <li>redis - Redis client</li>
     * </ul>
     *
     * @return List of PHP extension names
     */
    List<String> requiredPhpExtensions();

    /**
     * Returns PHP-FPM pool configuration overrides.
     *
     * <p>Common settings:</p>
     * <ul>
     *   <li>pm - Process manager (static, dynamic, ondemand)</li>
     *   <li>pm.max_children - Maximum worker processes</li>
     *   <li>pm.start_servers - Initial workers (dynamic mode)</li>
     *   <li>pm.min_spare_servers - Minimum idle workers</li>
     *   <li>pm.max_spare_servers - Maximum idle workers</li>
     *   <li>pm.max_requests - Requests before worker recycle</li>
     * </ul>
     *
     * @return Map of PHP-FPM configuration key-value pairs
     */
    default Map<String, String> phpFpmConfig() {
        return Map.of(
            "pm", "dynamic",
            "pm.max_children", "50",
            "pm.start_servers", "5",
            "pm.min_spare_servers", "5",
            "pm.max_spare_servers", "35",
            "pm.max_requests", "500"
        );
    }

    /**
     * Returns OPcache configuration for PHP bytecode caching.
     *
     * <p>Recommended production settings:</p>
     * <ul>
     *   <li>opcache.enable=1 - Enable OPcache</li>
     *   <li>opcache.memory_consumption=128 - Cache memory (MB)</li>
     *   <li>opcache.max_accelerated_files=10000 - Cached file limit</li>
     *   <li>opcache.revalidate_freq=60 - File check interval (seconds)</li>
     *   <li>opcache.validate_timestamps=0 - Disable for production</li>
     * </ul>
     *
     * @return Map of OPcache configuration key-value pairs
     */
    default Map<String, String> opcacheConfig() {
        return Map.of(
            "opcache.enable", "1",
            "opcache.memory_consumption", "128",
            "opcache.interned_strings_buffer", "16",
            "opcache.max_accelerated_files", "10000",
            "opcache.revalidate_freq", "60",
            "opcache.fast_shutdown", "1"
        );
    }

    /**
     * Returns PHP memory limit in megabytes.
     *
     * <p>Recommended values:</p>
     * <ul>
     *   <li>WordPress: 256MB</li>
     *   <li>WooCommerce: 512MB</li>
     *   <li>Magento: 756MB-2GB</li>
     *   <li>Drupal: 256MB</li>
     * </ul>
     *
     * @return Memory limit in MB
     */
    default int phpMemoryLimit() {
        return 256;
    }

    /**
     * Returns PHP max execution time in seconds.
     *
     * @return Max execution time (default: 300 seconds)
     */
    default int phpMaxExecutionTime() {
        return 300;
    }

    /**
     * Returns PHP upload max filesize in megabytes.
     *
     * @return Upload max filesize in MB (default: 64MB)
     */
    default int phpUploadMaxFilesize() {
        return 64;
    }

    /**
     * Returns PHP post max size in megabytes.
     *
     * @return Post max size in MB (default: 64MB)
     */
    default int phpPostMaxSize() {
        return 64;
    }

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

    /**
     * Returns whether S3 media offloading is supported.
     *
     * <p>When enabled, media uploads are stored in S3 instead of local
     * filesystem, enabling horizontal scaling and CDN integration.</p>
     *
     * @return true if S3 media storage is supported
     */
    default boolean supportsS3MediaStorage() {
        return false;
    }

    /**
     * Returns the plugin/module identifier for S3 media integration.
     *
     * <p>Examples:</p>
     * <ul>
     *   <li>WordPress: "wp-offload-media" or "amazon-s3-and-cloudfront"</li>
     *   <li>Magento: "magento/module-aws-s3"</li>
     *   <li>Drupal: "s3fs"</li>
     * </ul>
     *
     * @return Plugin identifier, or null if native S3 support
     */
    default String s3MediaPlugin() {
        return null;
    }

    /**
     * Returns the local media upload path within the container.
     *
     * <p>Examples:</p>
     * <ul>
     *   <li>WordPress: "/var/www/html/wp-content/uploads"</li>
     *   <li>Magento: "/var/www/html/pub/media"</li>
     *   <li>Drupal: "/var/www/html/sites/default/files"</li>
     * </ul>
     *
     * @return Absolute path to media upload directory
     */
    String mediaUploadPath();

    // ========== CDN Configuration ==========

    /**
     * Returns whether CDN integration is supported.
     *
     * <p>CDN integration via CloudFront enables:</p>
     * <ul>
     *   <li>Edge caching for static assets</li>
     *   <li>Global content delivery</li>
     *   <li>SSL termination at edge</li>
     *   <li>DDoS protection</li>
     * </ul>
     *
     * @return true if CDN integration is supported (default: true)
     */
    default boolean supportsCdnIntegration() {
        return true;
    }

    /**
     * Returns static asset paths to be served via CDN.
     *
     * <p>These paths will be configured as CloudFront cache behaviors
     * with optimized caching policies. Implementations should override
     * {@link #cdnStaticPaths()} instead; this method is kept for
     * backwards compatibility and defaults to {@code cdnStaticPaths()}.</p>
     *
     * @return List of URL path patterns (e.g., "/wp-content/themes/*")
     * @deprecated Override {@link #cdnStaticPaths()} instead
     */
    @Deprecated
    default List<String> cdnAssetPaths() {
        return cdnStaticPaths();
    }

    /**
     * Returns paths served from the S3 media origin in CloudFront.
     *
     * <p>These paths correspond to user-uploaded media files (images,
     * videos, documents) that are offloaded to S3. They will be routed
     * to the S3 origin with a long-TTL media cache policy.</p>
     *
     * <p>Examples:</p>
     * <ul>
     *   <li>WordPress: {@code ["/wp-content/uploads/*"]}</li>
     *   <li>Magento: {@code ["/media/*"]}</li>
     *   <li>Drupal: {@code ["/sites/default/files/*"]}</li>
     * </ul>
     *
     * @return List of URL path patterns for S3 media origin (default: derived from
     *         {@link #mediaUploadPath()}, empty if that path isn't under the web root)
     */
    default List<String> cdnMediaPaths() {
        // Derives a default from mediaUploadPath() so CDN media caching works without an
        // explicit override. Apps whose upload directory lives outside the web root (e.g.
        // Moodle's /var/moodledata, served through a script rather than directly) fall through
        // to the empty list.
        String webRoot = "/var/www/html";
        String uploadPath = mediaUploadPath();
        if (uploadPath != null && uploadPath.startsWith(webRoot + "/")) {
            return List.of(uploadPath.substring(webRoot.length()) + "/*");
        }
        return List.of();
    }

    /**
     * Returns static asset paths cached at the edge in CloudFront.
     *
     * <p>These paths (CSS, JS, fonts, theme assets) are served from the
     * ALB origin with a long-TTL static cache policy.  They do not include
     * user-uploaded media; see {@link #cdnMediaPaths()} for that.</p>
     *
     * <p>Examples:</p>
     * <ul>
     *   <li>WordPress: {@code ["/wp-content/themes/*", "/wp-content/plugins/*", "/wp-includes/*"]}</li>
     *   <li>Magento: {@code ["/static/*"]}</li>
     *   <li>Drupal: {@code ["/core/*", "/modules/*", "/themes/*"]}</li>
     * </ul>
     *
     * @return List of URL path patterns for edge-cached static assets (default: empty)
     */
    default List<String> cdnStaticPaths() {
        return List.of();
    }

    /**
     * Returns admin/back-office paths that bypass CDN caching entirely.
     *
     * <p>Requests to these paths are forwarded to the ALB with caching
     * disabled and all headers/cookies forwarded, ensuring the CMS admin
     * panel receives full session state.</p>
     *
     * <p>This is also the single source of truth for ALB OIDC path-based
     * authentication. The default {@link #protectedPaths()} implementation
     * below delegates here, so CMS implementations only need to override
     * {@code cdnAdminPaths()} and both CDN routing and ALB auth stay in sync.</p>
     *
     * <p>Examples:</p>
     * <ul>
     *   <li>WordPress: {@code ["/wp-admin/*", "/wp-login.php"]}</li>
     *   <li>Magento: {@code ["/admin/*", "/backend/*"]}</li>
     *   <li>Joomla: {@code ["/administrator/*"]}</li>
     * </ul>
     *
     * @return List of URL path patterns that must not be cached (default: empty)
     */
    default List<String> cdnAdminPaths() {
        return List.of();
    }

    /**
     * Returns paths requiring ALB OIDC authentication for this CMS.
     *
     * <p>Defaults to {@link #cdnAdminPaths()} so that CDN routing and
     * ALB-level Cognito authentication always protect the same paths.
     * Override only when the protected path set differs from the CDN admin paths.</p>
     *
     * @return list of path patterns requiring authentication
     */
    @Override
    default List<String> protectedPaths() {
        return cdnAdminPaths();
    }

    /**
     * Returns Redis connection environment variables for this CMS.
     *
     * <p>The default implementation returns the generic
     * {@code REDIS_HOST} / {@code REDIS_PORT} pair.  CMS implementations
     * should override to add their plugin-specific variable names.</p>
     *
     * <p>Examples of CMS-specific additions:</p>
     * <ul>
     *   <li>WordPress: {@code WP_REDIS_HOST}, {@code WP_REDIS_PORT}, {@code WP_REDIS_DATABASE}</li>
     *   <li>Magento: {@code MAGENTO_CACHE_BACKEND_REDIS_SERVER} (+ page-cache and session variants)</li>
     *   <li>Drupal: {@code DRUPAL_REDIS_HOST}, {@code DRUPAL_REDIS_PORT}</li>
     * </ul>
     *
     * @param host Redis primary endpoint hostname
     * @param port Redis port (usually 6379)
     * @return mutable map of environment variable key-value pairs
     */
    default Map<String, String> redisEnvVars(String host, int port) {
        Map<String, String> env = new java.util.HashMap<>();
        env.put("REDIS_HOST", host);
        env.put("REDIS_PORT", String.valueOf(port));
        return env;
    }

    /**
     * Returns database connection environment variables for this CMS.
     *
     * <p>The default implementation returns generic
     * {@code DB_HOST} / {@code DB_PORT} / {@code DB_NAME} / {@code DB_USER}.
     * CMS implementations should override to add their native variable names.</p>
     *
     * <p>Examples of CMS-specific additions:</p>
     * <ul>
     *   <li>WordPress: {@code WORDPRESS_DB_HOST}, {@code WORDPRESS_DB_NAME}, {@code WORDPRESS_DB_USER}</li>
     *   <li>Magento: {@code MAGENTO_DATABASE_HOST}, {@code MAGENTO_DATABASE_NAME}</li>
     *   <li>Drupal: {@code DRUPAL_DATABASE_HOST}, {@code DRUPAL_DATABASE_DRIVER}</li>
     * </ul>
     *
     * @param host   RDS endpoint hostname
     * @param port   database port (3306 for MySQL/MariaDB, 5432 for Postgres)
     * @param name   database name
     * @param user   database username
     * @return mutable map of environment variable key-value pairs
     */
    default Map<String, String> databaseEnvVars(String host, int port, String name, String user) {
        Map<String, String> env = new java.util.HashMap<>();
        env.put("DB_HOST", host);
        env.put("DB_PORT", String.valueOf(port));
        env.put("DB_NAME", name);
        env.put("DB_USER", user);
        return env;
    }

    // ========== Object Caching ==========

    /**
     * Returns whether Redis/Memcached object caching is supported.
     *
     * <p>Object caching stores database query results and computed
     * values in memory for faster retrieval.</p>
     *
     * @return true if object caching is supported
     */
    default boolean supportsObjectCache() {
        return false;
    }

    /**
     * Returns the preferred caching backend.
     *
     * @return "redis", "memcached", or "none"
     */
    default String preferredCacheBackend() {
        return "redis";
    }

    /**
     * Returns the object cache plugin/module identifier.
     *
     * <p>Examples:</p>
     * <ul>
     *   <li>WordPress: "redis-cache" (Redis Object Cache plugin)</li>
     *   <li>Magento: Built-in Redis support</li>
     *   <li>Drupal: "redis" module</li>
     * </ul>
     *
     * @return Plugin identifier for object caching
     */
    default String objectCachePlugin() {
        return null;
    }

    // ========== Scheduled Tasks ==========

    /**
     * Returns whether the CMS has scheduled tasks (cron jobs).
     *
     * <p>Most CMS platforms have internal task schedulers:</p>
     * <ul>
     *   <li>WordPress: WP-Cron</li>
     *   <li>Magento: Cron groups (index, default, consumers)</li>
     *   <li>Drupal: Cron module</li>
     * </ul>
     *
     * @return true if scheduled tasks exist
     */
    default boolean hasScheduledTasks() {
        return false;
    }

    /**
     * Returns whether to use system cron instead of internal scheduler.
     *
     * <p>System cron is recommended for production because:</p>
     * <ul>
     *   <li>More reliable execution timing</li>
     *   <li>Reduced page load overhead</li>
     *   <li>Better control over resource usage</li>
     * </ul>
     *
     * @return true to disable internal cron and use system cron
     */
    default boolean useSystemCron() {
        return true;
    }

    /**
     * Returns scheduled task commands for system cron.
     *
     * <p>Map keys are cron schedule expressions, values are commands.</p>
     *
     * <p>Example for WordPress:</p>
     * <pre>
     * "* /15 * * * *" -> "curl -s https://example.com/wp-cron.php"
     * </pre>
     *
     * @param siteUrl The site URL for cron execution
     * @return Map of cron schedule to command
     */
    default Map<String, String> cronCommands(String siteUrl) {
        return Map.of();
    }

    // ========== Multi-site Support ==========

    /**
     * Returns whether multi-site/multi-store is supported.
     *
     * <p>Multi-site capabilities:</p>
     * <ul>
     *   <li>WordPress: Multisite network</li>
     *   <li>Magento: Multi-store views</li>
     *   <li>Drupal: Multi-site configuration</li>
     *   <li>PrestaShop: Multi-shop</li>
     * </ul>
     *
     * @return true if multi-site is available
     */
    default boolean supportsMultisite() {
        return false;
    }

    /**
     * Returns the multi-site configuration mode.
     *
     * <p>Modes:</p>
     * <ul>
     *   <li>"subdomain" - sites.example.com</li>
     *   <li>"subdirectory" - example.com/sites/</li>
     *   <li>"domain" - separate domains per site</li>
     *   <li>"none" - multi-site not enabled</li>
     * </ul>
     *
     * @return Multi-site mode string
     */
    default String multisiteMode() {
        return "none";
    }

    // ========== CMS Category ==========

    /**
     * Returns the CMS category.
     *
     * <p>Categories:</p>
     * <ul>
     *   <li>"cms" - Content management (WordPress, Joomla, Drupal)</li>
     *   <li>"ecommerce" - E-commerce (WooCommerce, Magento, PrestaShop)</li>
     * </ul>
     *
     * @return "cms" or "ecommerce"
     */
    default String cmsCategory() {
        return "cms";
    }

    /**
     * Returns whether this is an e-commerce platform.
     *
     * <p>E-commerce platforms may require:</p>
     * <ul>
     *   <li>PCI-DSS compliance</li>
     *   <li>Higher resource allocation</li>
     *   <li>Payment gateway integration</li>
     *   <li>Inventory management</li>
     * </ul>
     *
     * @return true for e-commerce platforms
     */
    default boolean isEcommerce() {
        return "ecommerce".equals(cmsCategory());
    }

    // ========== CmsPlugin Metadata Methods ==========

    /**
     * Get the application category from @CmsPlugin.
     *
     * <p>Overrides {@link ApplicationSpec#category()} to read from
     * {@link CmsPlugin} annotation instead of ApplicationPlugin.</p>
     *
     * @return the category from @CmsPlugin (e.g., "cms", "ecommerce", "forum", "wiki", "lms", "social")
     */
    @Override
    default String category() {
        CmsPlugin annotation = getClass().getAnnotation(CmsPlugin.class);
        if (annotation == null) {
            return cmsCategory(); // Fall back to cmsCategory()
        }
        return annotation.category();
    }

    /**
     * Get the human-readable display name for this CMS.
     *
     * <p>Overrides {@link ApplicationSpec#displayName()} to read from
     * {@link CmsPlugin} annotation instead of ApplicationPlugin.</p>
     *
     * @return the display name from @CmsPlugin, or capitalized applicationId() if not specified
     */
    @Override
    default String displayName() {
        CmsPlugin annotation = getClass().getAnnotation(CmsPlugin.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 CMS description.
     *
     * <p>Overrides {@link ApplicationSpec#description()} to read from
     * {@link CmsPlugin} annotation instead of ApplicationPlugin.</p>
     *
     * @return the description from @CmsPlugin
     */
    @Override
    default String description() {
        CmsPlugin annotation = getClass().getAnnotation(CmsPlugin.class);
        if (annotation == null) {
            return "";
        }
        return annotation.description();
    }

    /**
     * Get the default Fargate CPU units from @CmsPlugin.
     *
     * @return the default CPU units
     */
    @Override
    default int defaultCpu() {
        CmsPlugin annotation = getClass().getAnnotation(CmsPlugin.class);
        if (annotation == null) {
            return 1024;
        }
        return annotation.defaultCpu();
    }

    /**
     * Get the default Fargate memory from @CmsPlugin.
     *
     * @return the default memory in MB
     */
    @Override
    default int defaultMemory() {
        CmsPlugin annotation = getClass().getAnnotation(CmsPlugin.class);
        if (annotation == null) {
            return 2048;
        }
        return annotation.defaultMemory();
    }

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

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

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

    /**
     * CMS apps declare database requirement on {@link CmsPlugin#requiresDatabase()}.
     */
    @Override
    default boolean requiresDatabase() {
        CmsPlugin annotation = getClass().getAnnotation(CmsPlugin.class);
        if (annotation == null) {
            return false;
        }
        return annotation.requiresDatabase();
    }

    /**
     * Check if this CMS supports OIDC integration.
     *
     * <p>Overrides {@link ApplicationSpec#supportsOidcIntegration()} to read from
     * {@link CmsPlugin} annotation instead of ApplicationPlugin.</p>
     *
     * @return true if OIDC is supported based on @CmsPlugin.supportsOidc()
     */
    @Override
    default boolean supportsOidcIntegration() {
        CmsPlugin annotation = getClass().getAnnotation(CmsPlugin.class);
        if (annotation == null) {
            return false;
        }
        return annotation.supportsOidc();
    }

    // ========== Web Server Configuration ==========

    /**
     * Returns the preferred web server.
     *
     * @return "nginx", "apache", or "caddy"
     */
    default String preferredWebServer() {
        return "nginx";
    }

    /**
     * Returns the document root path within the container.
     *
     * <p>This is where the web server serves files from.</p>
     *
     * @return Document root path (e.g., "/var/www/html")
     */
    default String documentRoot() {
        return containerDataPath();
    }

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

    /**
     * Returns the container startup command for this CMS.
     *
     * <p>Use this to override the default container entrypoint/command.
     * Common uses include:</p>
     * <ul>
     *   <li>Configuring web server to listen on non-privileged ports</li>
     *   <li>Setting up environment-specific configuration</li>
     *   <li>Running initialization scripts before the main process</li>
     * </ul>
     *
     * <p>Example for Apache on port 8080:</p>
     * <pre>
     * return List.of("/bin/sh", "-c",
     *     "sed -i 's/Listen 80/Listen 8080/' /etc/apache2/ports.conf &amp;&amp; " +
     *     "sed -i 's/:80/:8080/' /etc/apache2/sites-available/*.conf &amp;&amp; " +
     *     "apache2-foreground");
     * </pre>
     *
     * @return List of command arguments, or null to use the image default
     */
    default List<String> containerCommand() {
        return null;
    }

    // ========== CLI Tool Support ==========

    /**
     * Returns the CLI tool for this CMS, if available.
     *
     * <p>Examples:</p>
     * <ul>
     *   <li>WordPress: "wp" (WP-CLI)</li>
     *   <li>Magento: "bin/magento"</li>
     *   <li>Drupal: "drush"</li>
     *   <li>Joomla: "cli/joomla.php"</li>
     * </ul>
     *
     * @return CLI tool command, or null if not available
     */
    default String cliTool() {
        return null;
    }

    /**
     * Returns commands to install the CLI tool.
     *
     * @return List of shell commands to install CLI tool
     */
    default List<String> cliToolInstallCommands() {
        return List.of();
    }
}