PhpUserDataBuilder.java

package com.cloudforgeci.api.core;

import com.cloudforge.core.interfaces.CmsSpec;
import com.cloudforge.core.interfaces.PhpRuntimeConfig;
import com.cloudforge.core.interfaces.UserDataBuilder;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * UserData builder extension for PHP applications on EC2.
 *
 * <p>Provides helper methods for installing and configuring PHP,
 * NGINX, and CMS platforms on Amazon Linux 2023 EC2 instances.</p>
 *
 * <h2>Usage Example:</h2>
 * <pre>{@code
 * public void configureUserData(UserDataBuilder builder, Ec2Context context) {
 *     builder.addSystemUpdate();
 *
 *     PhpRuntimeConfig config = PhpRuntimeConfig.forWordPress();
 *     PhpUserDataBuilder.installPhp(builder, config);
 *     PhpUserDataBuilder.installNginx(builder);
 *     PhpUserDataBuilder.installWordPress(builder, "/var/www/html");
 *
 *     // Mount storage
 *     if (context.hasEfs()) {
 *         builder.mountEfs(...);
 *     }
 * }
 * }</pre>
 *
 * @since 3.1.0
 */
public final class PhpUserDataBuilder {

    private PhpUserDataBuilder() {
        // Utility class
    }

    /**
     * Install PHP and required extensions on Amazon Linux 2023.
     *
     * @param builder the UserDataBuilder
     * @param config the PHP runtime configuration
     */
    public static void installPhp(UserDataBuilder builder, PhpRuntimeConfig config) {
        String phpPkg = "php" + config.version();

        // Build extension package list
        String extensions = config.extensions().stream()
            .map(ext -> phpPkg + "-" + ext)
            .collect(Collectors.joining(" \\\n    "));

        builder.addCommands(
            "# Install PHP " + config.version(),
            "echo 'Installing PHP " + config.version() + "...' >> /var/log/userdata.log",
            "",
            "# Install PHP packages",
            "dnf install -y " + phpPkg + " \\",
            "    " + phpPkg + "-fpm \\",
            "    " + phpPkg + "-cli \\",
            "    " + extensions,
            "",
            "echo 'PHP installed successfully' >> /var/log/userdata.log"
        );

        // Configure PHP
        configurePhpIni(builder, config);
        configurePhpFpm(builder, config);
    }

    /**
     * Configure PHP memory limits, execution time, and other settings.
     *
     * @param builder the UserDataBuilder
     * @param config the PHP runtime configuration
     */
    public static void configurePhpIni(UserDataBuilder builder, PhpRuntimeConfig config) {
        builder.addCommands(
            "# Configure PHP settings",
            String.format("sed -i 's/memory_limit = .*/memory_limit = %dM/' /etc/php.ini",
                config.memoryLimit()),
            String.format("sed -i 's/max_execution_time = .*/max_execution_time = %d/' /etc/php.ini",
                config.maxExecutionTime()),
            String.format("sed -i 's/upload_max_filesize = .*/upload_max_filesize = %dM/' /etc/php.ini",
                config.uploadMaxFilesize()),
            String.format("sed -i 's/post_max_size = .*/post_max_size = %dM/' /etc/php.ini",
                config.postMaxSize()),
            "sed -i 's/max_input_vars = .*/max_input_vars = 10000/' /etc/php.ini",
            "sed -i 's/max_input_time = .*/max_input_time = 600/' /etc/php.ini",
            ""
        );

        // OPcache configuration
        if (!config.opcacheConfig().isEmpty()) {
            builder.addCommands("# Configure OPcache");
            for (Map.Entry<String, String> entry : config.opcacheConfig().entrySet()) {
                builder.addCommand(String.format(
                    "echo '%s = %s' >> /etc/php.d/10-opcache.ini",
                    entry.getKey(), entry.getValue()
                ));
            }
            builder.addCommand("");
        }

        // Additional php.ini settings
        if (!config.phpIni().isEmpty()) {
            builder.addCommands("# Additional PHP settings");
            for (Map.Entry<String, String> entry : config.phpIni().entrySet()) {
                builder.addCommand(String.format(
                    "echo '%s = %s' >> /etc/php.ini",
                    entry.getKey(), entry.getValue()
                ));
            }
            builder.addCommand("");
        }
    }

    /**
     * Configure PHP-FPM pool settings.
     *
     * @param builder the UserDataBuilder
     * @param config the PHP runtime configuration
     */
    public static void configurePhpFpm(UserDataBuilder builder, PhpRuntimeConfig config) {
        builder.addCommands(
            "# Configure PHP-FPM",
            "mkdir -p /var/log/php-fpm"
        );

        // Apply FPM configuration
        for (Map.Entry<String, String> entry : config.fpmConfig().entrySet()) {
            builder.addCommand(String.format(
                "sed -i 's/%s = .*/%s = %s/' /etc/php-fpm.d/www.conf",
                entry.getKey(), entry.getKey(), entry.getValue()
            ));
        }

        builder.addCommands(
            "",
            "# Enable and start PHP-FPM",
            "systemctl enable php-fpm",
            "systemctl start php-fpm",
            "echo 'PHP-FPM configured and started' >> /var/log/userdata.log"
        );
    }

    /**
     * Install NGINX web server.
     *
     * @param builder the UserDataBuilder
     */
    public static void installNginx(UserDataBuilder builder) {
        builder.addCommands(
            "# Install NGINX",
            "echo 'Installing NGINX...' >> /var/log/userdata.log",
            "dnf install -y nginx",
            "",
            "# Create log directories",
            "mkdir -p /var/log/nginx",
            "",
            "# Enable NGINX",
            "systemctl enable nginx",
            "echo 'NGINX installed' >> /var/log/userdata.log"
        );
    }

    /**
     * Configure NGINX with a provided configuration.
     *
     * @param builder the UserDataBuilder
     * @param nginxConfig the NGINX configuration content
     */
    public static void configureNginx(UserDataBuilder builder, String nginxConfig) {
        builder.addCommands(
            "# Configure NGINX",
            "cat > /etc/nginx/conf.d/cms.conf << 'NGINX_EOF'",
            nginxConfig,
            "NGINX_EOF",
            "",
            "# Test and start NGINX",
            "nginx -t && systemctl restart nginx",
            "echo 'NGINX configured and started' >> /var/log/userdata.log"
        );
    }

    /**
     * Install WP-CLI for WordPress management.
     *
     * @param builder the UserDataBuilder
     */
    public static void installWpCli(UserDataBuilder builder) {
        builder.addCommands(
            "# Install WP-CLI",
            "curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar",
            "chmod +x wp-cli.phar",
            "mv wp-cli.phar /usr/local/bin/wp",
            "echo 'WP-CLI installed' >> /var/log/userdata.log"
        );
    }

    /**
     * Download and install WordPress.
     *
     * @param builder the UserDataBuilder
     * @param installPath the installation directory
     */
    public static void installWordPress(UserDataBuilder builder, String installPath) {
        builder.addCommands(
            "# Download WordPress",
            "cd " + installPath,
            "wp core download --allow-root",
            "chown -R nginx:nginx " + installPath,
            "chmod -R 755 " + installPath,
            "echo 'WordPress downloaded' >> /var/log/userdata.log"
        );
    }

    /**
     * Install Composer for PHP dependency management.
     *
     * @param builder the UserDataBuilder
     */
    public static void installComposer(UserDataBuilder builder) {
        builder.addCommands(
            "# Install Composer",
            "php -r \"copy('https://getcomposer.org/installer', 'composer-setup.php');\"",
            "php composer-setup.php --install-dir=/usr/local/bin --filename=composer",
            "rm composer-setup.php",
            "echo 'Composer installed' >> /var/log/userdata.log"
        );
    }

    /**
     * Install Drush for Drupal management.
     *
     * @param builder the UserDataBuilder
     */
    public static void installDrush(UserDataBuilder builder) {
        builder.addCommands(
            "# Install Drush",
            "composer global require drush/drush",
            "ln -s ~/.composer/vendor/bin/drush /usr/local/bin/drush",
            "echo 'Drush installed' >> /var/log/userdata.log"
        );
    }

    /**
     * Install Magento CLI dependencies.
     *
     * @param builder the UserDataBuilder
     */
    public static void installMagentoDependencies(UserDataBuilder builder) {
        builder.addCommands(
            "# Install Magento dependencies",
            "dnf install -y elasticsearch opensearch",
            "systemctl enable elasticsearch",
            "systemctl start elasticsearch",
            "",
            "# Install Varnish for full-page caching",
            "dnf install -y varnish",
            "systemctl enable varnish",
            "echo 'Magento dependencies installed' >> /var/log/userdata.log"
        );
    }

    /**
     * Configure system cron for CMS scheduled tasks.
     *
     * @param builder the UserDataBuilder
     * @param spec the CMS specification
     * @param siteUrl the site URL
     */
    public static void configureCron(UserDataBuilder builder, CmsSpec spec, String siteUrl) {
        if (!spec.hasScheduledTasks()) {
            return;
        }

        Map<String, String> cronJobs = spec.cronCommands(siteUrl);
        if (cronJobs.isEmpty()) {
            return;
        }

        builder.addCommands(
            "# Configure CMS cron jobs",
            "crontab -l > /tmp/crontab.txt 2>/dev/null || true"
        );

        for (Map.Entry<String, String> cron : cronJobs.entrySet()) {
            builder.addCommand(String.format(
                "echo '%s %s' >> /tmp/crontab.txt",
                cron.getKey(), cron.getValue()
            ));
        }

        builder.addCommands(
            "crontab /tmp/crontab.txt",
            "rm /tmp/crontab.txt",
            "echo 'Cron jobs configured' >> /var/log/userdata.log"
        );
    }

    /**
     * Install Redis PHP extension and configure for object caching.
     *
     * @param builder the UserDataBuilder
     * @param redisHost Redis server hostname
     * @param redisPort Redis server port
     */
    public static void configureRedisCache(UserDataBuilder builder, String redisHost, int redisPort) {
        builder.addCommands(
            "# Configure Redis object cache",
            "echo 'Configuring Redis cache connection...' >> /var/log/userdata.log",
            "",
            "# Verify Redis connectivity",
            String.format("redis-cli -h %s -p %d ping", redisHost, redisPort),
            "echo 'Redis connection verified' >> /var/log/userdata.log"
        );
    }

    /**
     * Install and configure CloudWatch Agent for PHP logs.
     *
     * @param builder the UserDataBuilder
     * @param logGroupName CloudWatch log group name
     * @param spec the CMS specification
     */
    public static void installCloudWatchAgent(UserDataBuilder builder, String logGroupName, CmsSpec spec) {
        List<String> logPaths = new java.util.ArrayList<>(spec.ec2LogPaths());
        if (!logPaths.contains("/var/log/php-fpm/error.log"))  logPaths.add("/var/log/php-fpm/error.log");
        if (!logPaths.contains("/var/log/nginx/error.log"))    logPaths.add("/var/log/nginx/error.log");
        if (!logPaths.contains("/var/log/nginx/access.log"))   logPaths.add("/var/log/nginx/access.log");

        builder.installCloudWatchAgent(logGroupName, logPaths);
    }

    /**
     * Set up file permissions for CMS directory.
     *
     * @param builder the UserDataBuilder
     * @param documentRoot document root path
     * @param webUser web server user (e.g., "nginx", "www-data")
     * @param webGroup web server group
     */
    public static void setFilePermissions(UserDataBuilder builder, String documentRoot, String webUser, String webGroup) {
        builder.addCommands(
            "# Set file permissions",
            String.format("chown -R %s:%s %s", webUser, webGroup, documentRoot),
            String.format("find %s -type d -exec chmod 755 {} \\;", documentRoot),
            String.format("find %s -type f -exec chmod 644 {} \\;", documentRoot),
            "echo 'File permissions set' >> /var/log/userdata.log"
        );
    }

    /**
     * Configure SELinux for PHP/NGINX (if enabled).
     *
     * @param builder the UserDataBuilder
     * @param documentRoot document root path
     */
    public static void configureSELinux(UserDataBuilder builder, String documentRoot) {
        builder.addCommands(
            "# Configure SELinux (if enabled)",
            "if [ \"$(getenforce 2>/dev/null || true)\" = 'Enforcing' ]; then",
            "    setsebool -P httpd_can_network_connect 1",
            "    setsebool -P httpd_can_network_connect_db 1",
            "    setsebool -P httpd_unified 1",
            String.format("    chcon -R -t httpd_sys_rw_content_t %s", documentRoot),
            "    echo 'SELinux configured' >> /var/log/userdata.log",
            "fi"
        );
    }

    /**
     * Complete CMS installation with all standard components.
     *
     * @param builder the UserDataBuilder
     * @param spec the CMS specification
     * @param config the PHP runtime configuration
     * @param documentRoot document root path
     */
    public static void completeInstallation(
            UserDataBuilder builder,
            CmsSpec spec,
            PhpRuntimeConfig config,
            String documentRoot) {

        // System update
        builder.addSystemUpdate();

        // Install PHP
        installPhp(builder, config);

        // Install NGINX
        installNginx(builder);

        // Install Composer (needed for most modern CMS)
        installComposer(builder);

        // CMS-specific installations
        String cmsId = spec.applicationId();
        switch (cmsId) {
            case "wordpress", "woocommerce" -> installWpCli(builder);
            case "drupal" -> installDrush(builder);
            case "magento" -> installMagentoDependencies(builder);
        }

        // Set permissions
        setFilePermissions(builder, documentRoot, "nginx", "nginx");

        // Configure SELinux
        configureSELinux(builder, documentRoot);

        builder.addCommands(
            "",
            "echo 'CMS installation complete' >> /var/log/userdata.log",
            "echo 'Ready for configuration at: " + documentRoot + "' >> /var/log/userdata.log"
        );
    }
}