//! Deploy command — deploy a wwwhat project to production.
//!
//! Supports three targets:
//! - `ssh`: Build release binary, rsync to VPS, install systemd service
//! - `docker`: Generate Dockerfile, build image, optionally push
//! - `static`: Pre-render all pages to static HTML files

use anyhow::{Context, Result};
use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;

use crate::prerender;

// ---------------------------------------------------------------------------
// Deploy configuration (persisted in wwwhat.toml under [deploy])
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeployConfig {
    pub ssh: Option<SshConfig>,
    pub docker: Option<DockerConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SshConfig {
    pub host: String,
    pub user: String,
    pub remote_dir: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerConfig {
    pub image: String,
    pub registry: Option<String>,
}

// ---------------------------------------------------------------------------
// Deploy targets
// ---------------------------------------------------------------------------

/// Deploy to a remote server via SSH + rsync
pub async fn deploy_ssh(
    project_path: &Path,
    host: Option<&str>,
    user: Option<&str>,
    remote_dir: Option<&str>,
    dry_run: bool,
    yes: bool,
) -> Result<()> {
    let toml_path = project_path.join("wwwhat.toml");
    let saved = load_deploy_config(&toml_path);

    // Resolve config: CLI args > saved config > interactive prompt
    let host = resolve_or_prompt(
        host,
        saved
            .as_ref()
            .and_then(|c| c.ssh.as_ref().map(|s| s.host.as_str())),
        "SSH host (e.g., 77.42.22.50)",
    )?;
    let user = resolve_or_prompt(
        user,
        saved
            .as_ref()
            .and_then(|c| c.ssh.as_ref().map(|s| s.user.as_str())),
        "SSH user",
    )?;
    let remote_dir = resolve_or_prompt(
        remote_dir,
        saved
            .as_ref()
            .and_then(|c| c.ssh.as_ref().map(|s| s.remote_dir.as_str())),
        "Remote directory (e.g., /opt/wwwhat)",
    )?;

    // Save config for next time
    save_deploy_config(
        &toml_path,
        &DeployConfig {
            ssh: Some(SshConfig {
                host: host.clone(),
                user: user.clone(),
                remote_dir: remote_dir.clone(),
            }),
            docker: saved.and_then(|c| c.docker),
        },
    )?;

    println!();
    println!("  Deploy to {}@{}:{}", user, host, remote_dir);
    println!();

    if !yes && !dry_run {
        if !Confirm::with_theme(&ColorfulTheme::default())
            .with_prompt("  Proceed with deployment?")
            .default(true)
            .interact()?
        {
            println!("  Deployment cancelled.");
            return Ok(());
        }
    }

    // Step 1: Build release binary
    println!("  Building release binary...");
    let build_cmd = format!("cargo build --release --bin run-what");
    run_cmd(&build_cmd, dry_run)?;

    // Step 2: Rsync project files
    println!("  Syncing project files...");
    let project_str = project_path.to_string_lossy();
    let rsync_cmd = format!(
        "rsync -avz --exclude target --exclude .git --exclude '*.db' --exclude '*.db-journal' -e ssh {src}/ {user}@{host}:{dir}/project/",
        src = project_str,
        user = user,
        host = host,
        dir = remote_dir,
    );
    run_cmd(&rsync_cmd, dry_run)?;

    // Step 3: Copy release binary
    println!("  Copying binary...");
    let scp_cmd = format!(
        "scp target/release/run-what {}@{}:{}/run-what",
        user, host, remote_dir,
    );
    run_cmd(&scp_cmd, dry_run)?;

    // Step 4: Install systemd service
    println!("  Installing systemd service...");
    let service = format!(
        r#"[Unit]
Description=wwwhat web server
After=network.target

[Service]
ExecStart={dir}/run-what dev --path {dir}/project --host 0.0.0.0 --port 8085
Restart=always
WorkingDirectory={dir}

[Install]
WantedBy=multi-user.target"#,
        dir = remote_dir,
    );
    let install_cmd = format!(
        r#"ssh {}@{} "echo '{}' > /etc/systemd/system/wwwhat.service && systemctl daemon-reload && systemctl restart wwwhat""#,
        user,
        host,
        service.replace('\n', "\\n"),
    );
    run_cmd(&install_cmd, dry_run)?;

    println!();
    println!("  Deployment complete!");
    println!();

    Ok(())
}

/// Deploy via Docker — generate Dockerfile, build image, optionally push
pub async fn deploy_docker(
    project_path: &Path,
    image: Option<&str>,
    registry: Option<&str>,
    dry_run: bool,
    yes: bool,
) -> Result<()> {
    let toml_path = project_path.join("wwwhat.toml");
    let saved = load_deploy_config(&toml_path);

    let image = resolve_or_prompt(
        image,
        saved
            .as_ref()
            .and_then(|c| c.docker.as_ref().map(|s| s.image.as_str())),
        "Docker image name (e.g., wwwhat-app)",
    )?;

    // Save config
    save_deploy_config(
        &toml_path,
        &DeployConfig {
            ssh: saved.as_ref().and_then(|c| c.ssh.clone()),
            docker: Some(DockerConfig {
                image: image.clone(),
                registry: registry.map(String::from),
            }),
        },
    )?;

    // Generate Dockerfile if one doesn't exist
    let dockerfile_path = project_path.join("Dockerfile");
    if !dockerfile_path.exists() {
        println!("  Generating Dockerfile...");
        let dockerfile = generate_dockerfile();
        std::fs::write(&dockerfile_path, &dockerfile)?;
        println!("  Created {}", dockerfile_path.display());
    }

    // Generate .dockerignore if one doesn't exist
    let dockerignore_path = project_path.join(".dockerignore");
    if !dockerignore_path.exists() {
        let dockerignore = generate_dockerignore();
        std::fs::write(&dockerignore_path, &dockerignore)?;
        println!("  Created {}", dockerignore_path.display());
    }

    println!();
    println!("  Building Docker image: {}", image);

    if !yes && !dry_run {
        if !Confirm::with_theme(&ColorfulTheme::default())
            .with_prompt("  Proceed with Docker build?")
            .default(true)
            .interact()?
        {
            println!("  Cancelled.");
            return Ok(());
        }
    }

    // Build image
    let build_cmd = format!("docker build -t {} {}", image, project_path.display());
    run_cmd(&build_cmd, dry_run)?;

    // Push if registry specified
    if let Some(reg) = registry {
        let full_tag = format!("{}/{}", reg, image);
        println!("  Tagging and pushing to {}...", reg);
        run_cmd(&format!("docker tag {} {}", image, full_tag), dry_run)?;
        run_cmd(&format!("docker push {}", full_tag), dry_run)?;
    }

    println!();
    println!("  Docker build complete!");
    println!();

    Ok(())
}

/// Deploy as static site — pre-render all pages
pub async fn deploy_static(project_path: &Path, output: &Path, dry_run: bool) -> Result<()> {
    if dry_run {
        println!(
            "  [dry-run] Would pre-render {} to {}",
            project_path.display(),
            output.display()
        );
        return Ok(());
    }

    println!();
    println!("  Building static site...");
    println!();

    let result = prerender::prerender(prerender::PreRenderConfig {
        project_path: project_path.to_path_buf(),
        output_path: output.to_path_buf(),
        minify: true,
    })
    .await?;

    println!();
    println!("  Static site ready!");
    println!("  Pages: {}", result.pages_rendered);
    println!("  Size:  {}", crate::format_bytes(result.total_bytes));
    println!("  Output: {}", output.display());

    if !result.pages_skipped.is_empty() {
        println!("  Skipped:");
        for page in &result.pages_skipped {
            println!("    - {}", page);
        }
    }

    println!();
    println!("  Deploy to any static host:");
    println!(
        "    Cloudflare Pages:  npx wrangler pages deploy {}",
        output.display()
    );
    println!(
        "    Netlify:           netlify deploy --dir {}",
        output.display()
    );
    println!("    Vercel:            vercel --prod {}", output.display());
    println!();

    Ok(())
}

/// Interactive prompt to choose deploy target
pub fn choose_deploy_target() -> Result<String> {
    let choices = &[
        "static  - Pre-render to HTML files (Cloudflare Pages, Netlify, etc.)",
        "ssh     - Deploy to a VPS via SSH (Hetzner, DigitalOcean, etc.)",
        "docker  - Build a Docker image",
    ];

    println!();
    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("  Choose deploy target")
        .items(choices)
        .default(0)
        .interact()?;

    Ok(match selection {
        0 => "static".to_string(),
        1 => "ssh".to_string(),
        2 => "docker".to_string(),
        _ => unreachable!(),
    })
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Resolve a value from CLI arg, saved config, or interactive prompt
fn resolve_or_prompt(
    cli_value: Option<&str>,
    saved_value: Option<&str>,
    prompt: &str,
) -> Result<String> {
    if let Some(v) = cli_value {
        return Ok(v.to_string());
    }
    if let Some(v) = saved_value {
        return Ok(v.to_string());
    }
    let value: String = Input::with_theme(&ColorfulTheme::default())
        .with_prompt(format!("  {}", prompt))
        .interact_text()?;
    Ok(value)
}

/// Run a shell command, or print it in dry-run mode
fn run_cmd(cmd: &str, dry_run: bool) -> Result<()> {
    if dry_run {
        println!("  [dry-run] {}", cmd);
        return Ok(());
    }

    let status = Command::new("sh")
        .arg("-c")
        .arg(cmd)
        .status()
        .with_context(|| format!("Failed to execute: {}", cmd))?;

    if !status.success() {
        anyhow::bail!(
            "Command failed with exit code {}: {}",
            status.code().unwrap_or(-1),
            cmd
        );
    }

    Ok(())
}

/// Generate an optimized Dockerfile with cargo-chef caching
fn generate_dockerfile() -> String {
    r#"# Multi-stage build with cargo-chef for fast rebuilds
FROM lukemathwalker/cargo-chef:latest-rust-1.85 AS chef
WORKDIR /app

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release --bin run-what

FROM debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash wwwhat
WORKDIR /app

COPY --from=builder /app/target/release/run-what /usr/local/bin/run-what
COPY . /app/project

USER wwwhat
EXPOSE 8085
CMD ["run-what", "dev", "--path", "/app/project", "--host", "0.0.0.0", "--port", "8085"]
"#
    .to_string()
}

/// Generate a .dockerignore to exclude build artifacts and database files
fn generate_dockerignore() -> String {
    r#"target/
.git/
*.db
*.db-journal
*.db-shm
*.db-wal
"#
    .to_string()
}

/// Load deploy config from wwwhat.toml's [deploy] section
fn load_deploy_config(toml_path: &Path) -> Option<DeployConfig> {
    let content = std::fs::read_to_string(toml_path).ok()?;
    let doc = content.parse::<toml_edit::DocumentMut>().ok()?;
    let deploy = doc.get("deploy")?;
    let deploy_str = deploy.to_string();
    toml::from_str::<DeployConfig>(&deploy_str).ok()
}

/// Save deploy config to wwwhat.toml's [deploy] section (preserving existing content)
fn save_deploy_config(toml_path: &Path, config: &DeployConfig) -> Result<()> {
    let content = std::fs::read_to_string(toml_path).unwrap_or_default();
    let mut doc = content
        .parse::<toml_edit::DocumentMut>()
        .unwrap_or_else(|_| toml_edit::DocumentMut::new());

    // Serialize deploy config to a TOML table
    let deploy_str = toml::to_string(config)?;
    let deploy_doc = deploy_str.parse::<toml_edit::DocumentMut>()?;

    // Replace or insert [deploy] section
    doc["deploy"] = deploy_doc.as_item().clone();

    std::fs::write(toml_path, doc.to_string())?;
    Ok(())
}

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

    #[test]
    fn test_generate_dockerfile() {
        let df = generate_dockerfile();
        assert!(df.contains("cargo-chef"));
        assert!(df.contains("run-what"));
        assert!(df.contains("bookworm-slim"));
        assert!(df.contains("EXPOSE 8085"));
    }

    #[test]
    fn test_deploy_config_roundtrip() {
        let config = DeployConfig {
            ssh: Some(SshConfig {
                host: "1.2.3.4".to_string(),
                user: "root".to_string(),
                remote_dir: "/opt/wwwhat".to_string(),
            }),
            docker: Some(DockerConfig {
                image: "my-app".to_string(),
                registry: Some("ghcr.io/user".to_string()),
            }),
        };
        let serialized = toml::to_string(&config).unwrap();
        let deserialized: DeployConfig = toml::from_str(&serialized).unwrap();
        assert_eq!(deserialized.ssh.as_ref().unwrap().host, "1.2.3.4");
        assert_eq!(deserialized.docker.as_ref().unwrap().image, "my-app");
    }
}
