audb-cli 0.1.11

Command-line interface for AuDB database application framework
//! Deployment file templates
//!
//! This module contains templates for generating deployment files
//! (Dockerfile, docker-compose.yml, systemd service files, etc.).

use super::config::DeploymentConfig;

/// Generate a multi-stage Dockerfile for the project
pub fn generate_dockerfile(project_name: &str, config: &DeploymentConfig) -> String {
    let port = config.port.unwrap_or(8080);

    format!(
        r#"# Multi-stage Dockerfile for {project_name}
# Generated by AuDB

# Stage 1: Build
FROM rust:latest as builder

WORKDIR /build

# Copy manifests
COPY Cargo.toml Cargo.lock ./
COPY build.rs ./

# Copy source code
COPY src ./src
COPY gold ./gold

# Build release binary
RUN cargo build --release

# Stage 2: Runtime
FROM debian:bookworm-slim

# Install runtime dependencies
RUN apt-get update && \
    apt-get install -y ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# Create app user
RUN useradd -m -u 1000 app

WORKDIR /app

# Copy binary from builder
COPY --from=builder /build/target/release/{project_name} /app/{project_name}

# Create data directory
RUN mkdir -p /app/data && chown -R app:app /app

# Switch to app user
USER app

# Expose port
EXPOSE {port}

# Set environment
ENV RUST_LOG=info

# Run the application
CMD ["/app/{project_name}"]
"#,
        project_name = project_name,
        port = port
    )
}

/// Generate docker-compose.yml for the project
pub fn generate_docker_compose(project_name: &str, config: &DeploymentConfig) -> String {
    let port = config.port.unwrap_or(8080);
    let restart_policy = &config.restart;

    let mut compose = format!(
        r#"version: '3.8'

services:
  {project_name}:
    build: .
    image: {project_name}:latest
    container_name: {project_name}
    restart: {restart_policy}
    ports:
      - "{port}:{port}"
"#,
        project_name = project_name,
        port = port,
        restart_policy = restart_policy
    );

    // Add environment variables if any
    if !config.environment.is_empty() {
        compose.push_str("    environment:\n");
        for (key, value) in &config.environment {
            compose.push_str(&format!("      {}: {}\n", key, value));
        }
    }

    // Add volumes if any
    if !config.volumes.is_empty() {
        compose.push_str("    volumes:\n");
        for (host, container) in &config.volumes {
            compose.push_str(&format!("      - {}:{}\n", host, container));
        }
    }

    // Add health check if configured
    if let Some(ref healthcheck) = config.healthcheck {
        compose.push_str("    healthcheck:\n");
        compose.push_str(&format!(
            "      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:{}{}\"])\n",
            port, healthcheck.endpoint
        ));
        compose.push_str(&format!("      interval: {}\n", healthcheck.interval));
        compose.push_str(&format!("      timeout: {}\n", healthcheck.timeout));
        compose.push_str(&format!("      retries: {}\n", healthcheck.retries));
    }

    compose
}

/// Generate systemd service file
pub fn generate_systemd_service(
    project_name: &str,
    binary_path: &str,
    config: &DeploymentConfig,
) -> String {
    let port = config.port.unwrap_or(8080);
    let restart_policy = match config.restart.as_str() {
        "always" => "always",
        "on-failure" => "on-failure",
        "unless-stopped" => "always",
        _ => "on-failure",
    };

    let mut service = format!(
        r#"[Unit]
Description={project_name} - AuDB Application
After=network.target

[Service]
Type=simple
User=app
WorkingDirectory={working_dir}
ExecStart={binary_path}
Restart={restart_policy}
RestartSec=10

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/app/data

# Environment
Environment=RUST_LOG=info
Environment=PORT={port}
"#,
        project_name = project_name,
        working_dir = std::env::current_dir()
            .unwrap_or_default()
            .display()
            .to_string(),
        binary_path = binary_path,
        restart_policy = restart_policy,
        port = port
    );

    // Add custom environment variables
    for (key, value) in &config.environment {
        service.push_str(&format!("Environment={}={}\n", key, value));
    }

    service.push_str(
        r#"
[Install]
WantedBy=multi-user.target
"#,
    );

    service
}

/// Generate .dockerignore file
pub fn generate_dockerignore() -> String {
    r#"# Generated by AuDB
target/
.git/
.gitignore
*.log
*.md
.DS_Store
.audb/
"#
    .to_string()
}

/// Generate healthcheck script for Docker
pub fn generate_healthcheck_script(port: u16, endpoint: &str) -> String {
    format!(
        r#"#!/bin/sh
# Health check script for Docker
curl -f http://localhost:{}{} || exit 1
"#,
        port, endpoint
    )
}

/// Generate macOS launchd plist file
pub fn generate_launchd_plist(
    service_label: &str,
    binary_path: &std::path::Path,
    config: &DeploymentConfig,
) -> String {
    let port = config.port.unwrap_or(8080);

    let mut env_vars = String::new();
    if !config.environment.is_empty() {
        env_vars.push_str("    <key>EnvironmentVariables</key>\n");
        env_vars.push_str("    <dict>\n");
        for (key, value) in &config.environment {
            env_vars.push_str(&format!("        <key>{}</key>\n", key));
            env_vars.push_str(&format!("        <string>{}</string>\n", value));
        }
        env_vars.push_str(&format!("        <key>PORT</key>\n"));
        env_vars.push_str(&format!("        <string>{}</string>\n", port));
        env_vars.push_str("    </dict>\n");
    } else {
        env_vars.push_str("    <key>EnvironmentVariables</key>\n");
        env_vars.push_str("    <dict>\n");
        env_vars.push_str(&format!("        <key>PORT</key>\n"));
        env_vars.push_str(&format!("        <string>{}</string>\n", port));
        env_vars.push_str("    </dict>\n");
    }

    format!(
        r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>{service_label}</string>

    <key>ProgramArguments</key>
    <array>
        <string>{binary_path}</string>
    </array>

    <key>RunAtLoad</key>
    <{run_at_load}/>

    <key>KeepAlive</key>
    <{keep_alive}/>

{env_vars}

    <key>StandardOutPath</key>
    <string>/tmp/{service_label}.log</string>

    <key>StandardErrorPath</key>
    <string>/tmp/{service_label}.err</string>

    <key>WorkingDirectory</key>
    <string>{working_dir}</string>
</dict>
</plist>
"#,
        service_label = service_label,
        binary_path = binary_path.display(),
        run_at_load = if config.persist { "true" } else { "false" },
        keep_alive = if config.persist { "true" } else { "false" },
        env_vars = env_vars.trim_end(),
        working_dir = binary_path
            .parent()
            .and_then(|p| p.parent())
            .and_then(|p| p.parent())
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| "/tmp".to_string())
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    #[test]
    fn test_generate_dockerfile() {
        let config = DeploymentConfig {
            port: Some(3000),
            ..Default::default()
        };

        let dockerfile = generate_dockerfile("my-app", &config);

        assert!(dockerfile.contains("my-app"));
        assert!(dockerfile.contains("EXPOSE 3000"));
        assert!(dockerfile.contains("rust:latest"));
        assert!(dockerfile.contains("debian:bookworm-slim"));
    }

    #[test]
    fn test_generate_docker_compose() {
        let mut config = DeploymentConfig::default();
        config.port = Some(8080);
        config.restart = "always".to_string();

        let compose = generate_docker_compose("test-app", &config);

        assert!(compose.contains("test-app"));
        assert!(compose.contains("8080:8080"));
        assert!(compose.contains("restart: always"));
    }

    #[test]
    fn test_generate_docker_compose_with_env() {
        let mut config = DeploymentConfig::default();
        config
            .environment
            .insert("DB_URL".to_string(), "postgres://localhost".to_string());

        let compose = generate_docker_compose("test-app", &config);

        assert!(compose.contains("environment:"));
        assert!(compose.contains("DB_URL: postgres://localhost"));
    }

    #[test]
    fn test_generate_systemd_service() {
        let config = DeploymentConfig {
            port: Some(8080),
            restart: "on-failure".to_string(),
            ..Default::default()
        };

        let service = generate_systemd_service("my-service", "/usr/local/bin/my-service", &config);

        assert!(service.contains("my-service"));
        assert!(service.contains("ExecStart=/usr/local/bin/my-service"));
        assert!(service.contains("Restart=on-failure"));
        assert!(service.contains("PORT=8080"));
    }

    #[test]
    fn test_generate_dockerignore() {
        let ignore = generate_dockerignore();
        assert!(ignore.contains("target/"));
        assert!(ignore.contains(".git/"));
        assert!(ignore.contains(".audb/"));
    }

    #[test]
    fn test_generate_launchd_plist() {
        use std::path::PathBuf;

        let config = DeploymentConfig {
            port: Some(8080),
            persist: true,
            ..Default::default()
        };

        let plist = generate_launchd_plist(
            "com.audb.test",
            &PathBuf::from("/usr/local/bin/test-app"),
            &config,
        );

        assert!(plist.contains("com.audb.test"));
        assert!(plist.contains("/usr/local/bin/test-app"));
        assert!(plist.contains("<key>RunAtLoad</key>"));
        assert!(plist.contains("<key>KeepAlive</key>"));
        assert!(plist.contains("<key>PORT</key>"));
        assert!(plist.contains("<string>8080</string>"));
        assert!(plist.contains("/tmp/com.audb.test.log"));
    }
}