use super::RpcHandler;
use crate::port_allocator::PortAllocator;
use anyhow::{Context, Result};
use tracing::{debug, info};

impl RpcHandler {
    /// Install a package from a Docker image
    /// Security: Image verification, resource limits, network isolation
    pub(super) async fn handle_package_install(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;

        let package_id = params
            .get("id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing package id"))?;
        validate_app_id(package_id)?;

        let docker_image = params
            .get("dockerImage")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing dockerImage"))?;

        debug!("Installing package {} from image {}", package_id, docker_image);

        // Security: Validate image name format (prevent injection)
        if !is_valid_docker_image(docker_image) {
            return Err(anyhow::anyhow!("Invalid Docker image format"));
        }

        // Virtual app: Indeehub (no container, opens external URL)
        if package_id == "indeedhub" {
            return Ok(serde_json::json!({ "success": true }));
        }

        // Multi-container apps: create full stack
        if package_id == "immich" {
            return self.install_immich_stack().await;
        }
        if package_id == "penpot" || package_id == "penpot-frontend" {
            return self.install_penpot_stack().await;
        }

        // Dependency check: electrs requires a Bitcoin node
        if package_id == "mempool-electrs" || package_id == "electrs" {
            let btc_check = tokio::process::Command::new("sudo")
                .args(["podman", "ps", "--format", "{{.Names}}"])
                .output()
                .await
                .context("Failed to check running containers")?;
            let running = String::from_utf8_lossy(&btc_check.stdout);
            let has_bitcoin = running.lines().any(|l| {
                let name = l.trim();
                name == "bitcoin-knots" || name == "bitcoin-core" || name == "bitcoin"
            });
            if !has_bitcoin {
                return Err(anyhow::anyhow!(
                    "Electrs requires a running Bitcoin node (Bitcoin Knots or Bitcoin Core). Please install and start Bitcoin Knots first."
                ));
            }
        }

        // Check if container already exists
        let check_output = tokio::process::Command::new("sudo")
            .args(["podman", "ps", "-a", "--format", "{{.Names}}", "--filter", &format!("name=^{}$", package_id)])
            .output()
            .await
            .context("Failed to check existing containers")?;

        if !String::from_utf8_lossy(&check_output.stdout).trim().is_empty() {
            return Err(anyhow::anyhow!("Container {} already exists. Stop and remove it first.", package_id));
        }

        // Pull the image (skip for local images - must be built locally first)
        let is_local_image = docker_image.starts_with("localhost/");
        if !is_local_image {
            debug!("Pulling image: {}", docker_image);
            let pull_output = tokio::process::Command::new("sudo")
                .args(["podman", "pull", docker_image])
                .output()
                .await
                .context("Failed to pull image")?;

            if !pull_output.status.success() {
                let stderr = String::from_utf8_lossy(&pull_output.stderr);
                return Err(anyhow::anyhow!("Failed to pull image: {}", stderr));
            }
        } else {
            // Verify local image exists
            let images_output = tokio::process::Command::new("sudo")
                .args(["podman", "images", "-q", docker_image])
                .output()
                .await
                .context("Failed to check local image")?;
            if String::from_utf8_lossy(&images_output.stdout).trim().is_empty() {
                return Err(anyhow::anyhow!(
                    "Local image {} not found. Run ./deploy-to-archipelago.sh from the Indeehub Prototype project on your Mac—it builds the image on this server and starts the app.",
                    docker_image
                ));
            }
            debug!("Using local image: {}", docker_image);
        }

        // Normalize container name: "electrs" alias -> "mempool-electrs"
        let container_name = if package_id == "electrs" { "mempool-electrs" } else { package_id };

        // Create and start container with security constraints
        let mut run_args = vec![
            "podman", "run",
            "-d",  // Detached
            "--name", container_name,
            "--restart=unless-stopped",  // Auto-restart policy
        ];

        // App-specific configuration (should come from manifest)
        let (ports, volumes, env_vars, custom_command, custom_args) = {
            let mut allocator = self.port_allocator.lock().map_err(|e| {
                anyhow::anyhow!("Port allocator lock poisoned: {}", e)
            })?;
            get_app_config(package_id, &self.config.host_ip, &mut allocator)
        };

        // Special handling: Tailscale needs host network; mempool stack needs archy-net
        let is_tailscale = package_id == "tailscale";
        let needs_archy_net = matches!(
            package_id,
            "bitcoin-knots" | "bitcoin" | "bitcoin-core"
                | "mempool" | "mempool-web" | "mempool-api" | "mempool-electrs" | "electrs" | "mysql-mempool" | "archy-mempool-db" | "archy-mempool-web"
                | "btcpay-server" | "btcpayserver" | "archy-btcpay-db"
        );

        if is_tailscale {
            run_args.push("--network=host");
            run_args.push("--privileged");
            run_args.push("--cap-add=NET_ADMIN");
            run_args.push("--cap-add=NET_RAW");
            run_args.push("--device=/dev/net/tun");
        } else if needs_archy_net {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "network", "create", "archy-net"])
                .output()
                .await;
            run_args.push("--network=archy-net");
        }

        // Security hardening (skip for privileged containers like Tailscale)
        let security_caps: Vec<String> = if !is_tailscale {
            get_app_capabilities(package_id)
        } else {
            vec![]
        };
        let readonly_compatible = !is_tailscale && is_readonly_compatible(package_id);

        if !is_tailscale {
            run_args.push("--cap-drop=ALL");
            run_args.push("--security-opt=no-new-privileges:true");
            for cap in &security_caps {
                run_args.push(cap);
            }
            if readonly_compatible {
                run_args.push("--read-only");
                run_args.push("--tmpfs=/tmp:rw,noexec,nosuid,size=256m");
                run_args.push("--tmpfs=/run:rw,noexec,nosuid,size=64m");
            }
        }

        // Create data directories if they don't exist
        for volume in &volumes {
            if let Some(host_path) = volume.split(':').next() {
                if host_path.starts_with("/var/lib/archipelago/") {
                    debug!("Creating directory: {}", host_path);
                    let create_dir = tokio::process::Command::new("sudo")
                        .args(["mkdir", "-p", host_path])
                        .output()
                        .await;

                    if let Err(e) = create_dir {
                        debug!("Failed to create directory {}: {}", host_path, e);
                    }
                    // Grafana runs as UID 472 - fix permissions so it can write
                    if package_id == "grafana" && host_path.contains("grafana") {
                        let _ = tokio::process::Command::new("sudo")
                            .args(["chown", "-R", "472:472", host_path])
                            .output()
                            .await;
                    }
                }
            }
        }

        // Pre-install: Create bitcoin.conf for Bitcoin nodes with RPC + txindex
        if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
            let bitcoin_dir = "/var/lib/archipelago/bitcoin";
            let conf_path = format!("{}/bitcoin.conf", bitcoin_dir);
            let bitcoin_conf = "\
server=1\n\
txindex=1\n\
rpcuser=archipelago\n\
rpcpassword=archipelago123\n\
rpcbind=0.0.0.0\n\
rpcallowip=0.0.0.0/0\n\
rpcport=8332\n\
listen=1\n\
printtoconsole=1\n";
            let _ = tokio::process::Command::new("sudo")
                .args(["sh", "-c", &format!("echo '{}' > {}", bitcoin_conf, conf_path)])
                .output()
                .await;
            info!("Created bitcoin.conf at {} with RPC + txindex enabled", conf_path);
        }

        // Add port mappings (skip if host network mode like Tailscale)
        if !is_tailscale {
            for port in &ports {
                run_args.push("-p");
                run_args.push(port);
            }
        }

        // Add volume mounts
        for volume in &volumes {
            run_args.push("-v");
            run_args.push(volume);
        }

        // Add environment variables
        for env in &env_vars {
            run_args.push("-e");
            run_args.push(env);
        }

        // Security: Resource limits (from manifest)
        let memory_limit = if package_id == "ollama" { "4g" } else { "2g" };
        let mem_arg = format!("--memory={}", memory_limit);
        run_args.push(&mem_arg);
        run_args.push("--cpus=2");

        // Finally, the image
        run_args.push(docker_image);

        debug!("Running container with args: {:?}", run_args);

        // Build command with optional custom command
        let mut cmd = tokio::process::Command::new("sudo");
        cmd.args(&run_args);

        // Add custom command/args if specified
        if let Some(custom_cmd) = custom_command {
            cmd.arg(custom_cmd);
        } else if let Some(args) = custom_args {
            cmd.args(args);
        }

        let run_output = cmd
            .output()
            .await
            .context("Failed to run container")?;

        if !run_output.status.success() {
            let stderr = String::from_utf8_lossy(&run_output.stderr);
            return Err(anyhow::anyhow!("Failed to start container: {}", stderr));
        }

        let container_id = String::from_utf8_lossy(&run_output.stdout).trim().to_string();

        // Post-install: Nextcloud needs trusted domains configured for iframe embedding
        if package_id == "nextcloud" {
            let host_ip = self.config.host_ip.clone();
            tokio::spawn(async move {
                // Wait for Nextcloud to finish first-run initialization
                tokio::time::sleep(std::time::Duration::from_secs(30)).await;
                for domain_idx in 1..=2u8 {
                    let value = if domain_idx == 1 { host_ip.as_str() } else { "localhost" };
                    let _ = tokio::process::Command::new("sudo")
                        .args([
                            "podman", "exec", "-u", "33", "nextcloud",
                            "php", "occ", "config:system:set",
                            "trusted_domains", &domain_idx.to_string(),
                            "--value", value,
                        ])
                        .output()
                        .await;
                }
                info!("Nextcloud trusted domains configured for {}", host_ip);
            });
        }

        // Post-install: Start electrs-ui container for electrs
        if matches!(package_id, "mempool-electrs" | "electrs") {
            tokio::spawn(async move {
                // Build and start electrs-ui with host networking so it can reach backend on 127.0.0.1:5678
                let ui_dir = "/opt/archipelago/docker/electrs-ui";
                let _ = tokio::process::Command::new("sudo")
                    .args(["podman", "build", "-t", "localhost/electrs-ui", ui_dir])
                    .output()
                    .await;
                // Remove old UI container if it exists
                let _ = tokio::process::Command::new("sudo")
                    .args(["podman", "rm", "-f", "electrs-ui"])
                    .output()
                    .await;
                let _ = tokio::process::Command::new("sudo")
                    .args([
                        "podman", "run", "-d", "--name", "electrs-ui",
                        "--restart=unless-stopped", "--network=host",
                        "localhost/electrs-ui",
                    ])
                    .output()
                    .await;
                info!("Electrs UI container started on port 50002");
            });
        }

        Ok(serde_json::json!({
            "success": true,
            "package_id": package_id,
            "container_id": container_id,
            "message": format!("Package {} installed and started", package_id)
        }))
    }

    /// Install Immich stack (postgres + redis + server)
    async fn install_immich_stack(&self) -> Result<serde_json::Value> {
        let check = tokio::process::Command::new("sudo")
            .args(["podman", "ps", "-a", "--format", "{{.Names}}"])
            .output()
            .await
            .context("Failed to list containers")?;
        let stdout = String::from_utf8_lossy(&check.stdout);
        if stdout.contains("immich_server") {
            return Err(anyhow::anyhow!("Immich already installed. Stop and remove it first."));
        }
        if stdout.contains("immich\n") || stdout.lines().any(|l| l.trim() == "immich") {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "stop", "immich"])
                .output()
                .await;
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "rm", "-f", "immich"])
                .output()
                .await;
        }

        let images = [
            "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0",
            "docker.io/valkey/valkey:7-alpine",
            "ghcr.io/immich-app/immich-server:release",
        ];
        for img in &images {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "pull", img])
                .output()
                .await;
        }

        let _ = tokio::process::Command::new("sudo")
            .args(["mkdir", "-p", "/var/lib/archipelago/immich", "/var/lib/archipelago/immich-db"])
            .output()
            .await;
        let _ = tokio::process::Command::new("sudo")
            .args(["podman", "network", "create", "immich-net"])
            .output()
            .await;

        let _ = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "immich_postgres", "--restart", "unless-stopped",
                "--network", "immich-net",
                "-v", "/var/lib/archipelago/immich-db:/var/lib/postgresql/data",
                "-e", "POSTGRES_PASSWORD=immichpass", "-e", "POSTGRES_USER=postgres", "-e", "POSTGRES_DB=immich",
                "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0",
            ])
            .output()
            .await;
        tokio::time::sleep(std::time::Duration::from_secs(5)).await;

        let _ = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "immich_redis", "--restart", "unless-stopped",
                "--network", "immich-net",
                "docker.io/valkey/valkey:7-alpine",
            ])
            .output()
            .await;
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;

        let run = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "immich_server", "--restart", "unless-stopped",
                "--network", "immich-net", "-p", "2283:2283",
                "-v", "/var/lib/archipelago/immich:/usr/src/app/upload",
                "-e", "DB_HOSTNAME=immich_postgres", "-e", "DB_USERNAME=postgres",
                "-e", "DB_PASSWORD=immichpass", "-e", "DB_DATABASE_NAME=immich",
                "-e", "REDIS_HOSTNAME=immich_redis", "-e", "UPLOAD_LOCATION=/usr/src/app/upload",
                "ghcr.io/immich-app/immich-server:release",
            ])
            .output()
            .await
            .context("Failed to start immich_server")?;

        if !run.status.success() {
            let stderr = String::from_utf8_lossy(&run.stderr);
            return Err(anyhow::anyhow!("Failed to start Immich server: {}", stderr));
        }

        Ok(serde_json::json!({
            "success": true,
            "package_id": "immich",
            "message": "Immich stack installed and started"
        }))
    }

    /// Install Penpot stack (postgres + valkey + backend + exporter + frontend)
    async fn install_penpot_stack(&self) -> Result<serde_json::Value> {
        let check = tokio::process::Command::new("sudo")
            .args(["podman", "ps", "-a", "--format", "{{.Names}}"])
            .output()
            .await
            .context("Failed to list containers")?;
        let stdout = String::from_utf8_lossy(&check.stdout);
        if stdout.contains("penpot-frontend") {
            return Err(anyhow::anyhow!("Penpot already installed. Stop and remove it first."));
        }

        let images = [
            "docker.io/postgres:15",
            "docker.io/valkey/valkey:8.1",
            "docker.io/penpotapp/backend:latest",
            "docker.io/penpotapp/exporter:latest",
            "docker.io/penpotapp/frontend:latest",
        ];
        for img in &images {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "pull", img])
                .output()
                .await;
        }

        let _ = tokio::process::Command::new("sudo")
            .args(["mkdir", "-p", "/var/lib/archipelago/penpot-assets"])
            .output()
            .await;
        let _ = tokio::process::Command::new("sudo")
            .args(["podman", "network", "create", "penpot-net"])
            .output()
            .await;

        let secret = "archipelago-penpot-secret-key-change-in-production";
        let host_ip = &self.config.host_ip;

        let _ = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "penpot-postgres", "--restart", "unless-stopped",
                "--network", "penpot-net",
                "-v", "/var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data",
                "-e", "POSTGRES_DB=penpot", "-e", "POSTGRES_USER=penpot", "-e", "POSTGRES_PASSWORD=penpot",
                "docker.io/postgres:15",
            ])
            .output()
            .await;
        tokio::time::sleep(std::time::Duration::from_secs(5)).await;

        let _ = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "penpot-valkey", "--restart", "unless-stopped",
                "--network", "penpot-net",
                "-e", "VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu",
                "docker.io/valkey/valkey:8.1",
            ])
            .output()
            .await;
        tokio::time::sleep(std::time::Duration::from_secs(3)).await;

        let _ = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "penpot-backend", "--restart", "unless-stopped",
                "--network", "penpot-net",
                "-v", "/var/lib/archipelago/penpot-assets:/opt/data/assets",
                "-e", &format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip),
                "-e", &format!("PENPOT_SECRET_KEY={}", secret),
                "-e", "PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot",
                "-e", "PENPOT_DATABASE_USERNAME=penpot", "-e", "PENPOT_DATABASE_PASSWORD=penpot",
                "-e", "PENPOT_REDIS_URI=redis://penpot-valkey/0",
                "-e", "PENPOT_OBJECTS_STORAGE_BACKEND=fs",
                "-e", "PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets",
                "-e", "PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies",
                "docker.io/penpotapp/backend:latest",
            ])
            .output()
            .await;
        tokio::time::sleep(std::time::Duration::from_secs(5)).await;

        let _ = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "penpot-exporter", "--restart", "unless-stopped",
                "--network", "penpot-net",
                "-e", &format!("PENPOT_SECRET_KEY={}", secret),
                "-e", "PENPOT_PUBLIC_URI=http://penpot-frontend:8080",
                "-e", "PENPOT_REDIS_URI=redis://penpot-valkey/0",
                "docker.io/penpotapp/exporter:latest",
            ])
            .output()
            .await;
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;

        let run = tokio::process::Command::new("sudo")
            .args([
                "podman", "run", "-d", "--name", "penpot-frontend", "--restart", "unless-stopped",
                "--network", "penpot-net", "-p", "9001:8080",
                "-v", "/var/lib/archipelago/penpot-assets:/opt/data/assets",
                "-e", &format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip),
                "-e", "PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies",
                "docker.io/penpotapp/frontend:latest",
            ])
            .output()
            .await
            .context("Failed to start penpot-frontend")?;

        if !run.status.success() {
            let stderr = String::from_utf8_lossy(&run.stderr);
            return Err(anyhow::anyhow!("Failed to start Penpot frontend: {}", stderr));
        }

        Ok(serde_json::json!({
            "success": true,
            "package_id": "penpot",
            "message": "Penpot stack installed and started"
        }))
    }

    pub(super) async fn handle_package_start(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let package_id = params
            .get("id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing package id"))?;

        let containers = get_containers_for_app(package_id).await?;
        let to_start: Vec<String> = if containers.is_empty() {
            vec![format!("archy-{}", package_id)]
        } else {
            let order: &[&str] = match package_id {
                "mempool" | "mempool-web" => &["archy-mempool-db", "mysql-mempool", "mempool-electrs", "mempool-api", "archy-mempool-api", "archy-mempool-web", "mempool"],
                "immich" => &["immich_postgres", "immich_redis", "immich_server"],
                "penpot" | "penpot-frontend" => &["penpot-postgres", "penpot-valkey", "penpot-backend", "penpot-exporter", "penpot-frontend"],
                _ => &["archy-mempool-db", "mysql-mempool", "mempool-electrs", "mempool-api", "archy-mempool-api", "archy-mempool-web", "mempool"],
            };
            let mut sorted = containers;
            sorted.sort_by_key(|c| order.iter().position(|o| *o == c).unwrap_or(99));
            sorted
        };

        for name in to_start {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "start", &name])
                .output()
                .await;
        }

        Ok(serde_json::Value::Null)
    }

    pub(super) async fn handle_package_stop(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let package_id = params
            .get("id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing package id"))?;

        let containers = get_containers_for_app(package_id).await?;
        if containers.is_empty() {
            let container_name = format!("archy-{}", package_id);
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "stop", &container_name])
                .output()
                .await;
            return Ok(serde_json::Value::Null);
        }

        for name in containers {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "stop", &name])
                .output()
                .await;
        }

        Ok(serde_json::Value::Null)
    }

    pub(super) async fn handle_package_restart(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let package_id = params
            .get("id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing package id"))?;

        let containers = get_containers_for_app(package_id).await?;
        if containers.is_empty() {
            let container_name = format!("archy-{}", package_id);
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "restart", &container_name])
                .output()
                .await;
            return Ok(serde_json::Value::Null);
        }

        for name in containers {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "restart", &name])
                .output()
                .await;
        }

        Ok(serde_json::Value::Null)
    }

    /// Uninstall a package: stop and remove all related containers, clean data.
    pub(super) async fn handle_package_uninstall(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let package_id = params
            .get("id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing package id"))?;
        validate_app_id(package_id)?;
        let preserve_data = params
            .get("preserve_data")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);

        let containers_to_remove = get_containers_for_app(package_id).await?;

        for name in &containers_to_remove {
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "stop", name])
                .output()
                .await;
            let _ = tokio::process::Command::new("sudo")
                .args(["podman", "rm", "-f", name])
                .output()
                .await;
        }

        // Release port allocation
        if let Ok(mut allocator) = self.port_allocator.lock() {
            let _ = allocator.release(package_id);
        }

        // Clean data directories unless preserve_data
        if !preserve_data {
            let data_dirs = get_data_dirs_for_app(package_id);
            for dir in &data_dirs {
                let _ = tokio::process::Command::new("sudo")
                    .args(["rm", "-rf", dir])
                    .output()
                    .await;
            }
        }

        Ok(serde_json::json!({ "status": "uninstalled" }))
    }

    /// Start a bundled app (create container from pre-loaded image if needed, then start)
    pub(super) async fn handle_bundled_app_start(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let app_id = params
            .get("app_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
        let image = params
            .get("image")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing image"))?;
        let ports = params
            .get("ports")
            .and_then(|v| v.as_array())
            .ok_or_else(|| anyhow::anyhow!("Missing ports"))?;
        let volumes = params
            .get("volumes")
            .and_then(|v| v.as_array())
            .ok_or_else(|| anyhow::anyhow!("Missing volumes"))?;

        let check_output = tokio::process::Command::new("sudo")
            .args(["podman", "ps", "-a", "--format", "{{.Names}}", "--filter", &format!("name={}", app_id)])
            .output()
            .await
            .context("Failed to check container")?;

        let existing = String::from_utf8_lossy(&check_output.stdout);

        if existing.trim().is_empty() {
            let mut cmd = tokio::process::Command::new("sudo");
            cmd.args(["podman", "run", "-d", "--name", app_id]);

            for port in ports {
                if let (Some(host), Some(container)) = (
                    port.get("host").and_then(|v| v.as_u64()),
                    port.get("container").and_then(|v| v.as_u64()),
                ) {
                    cmd.arg("-p").arg(format!("{}:{}", host, container));
                }
            }

            for volume in volumes {
                if let (Some(host), Some(container)) = (
                    volume.get("host").and_then(|v| v.as_str()),
                    volume.get("container").and_then(|v| v.as_str()),
                ) {
                    let _ = tokio::process::Command::new("sudo")
                        .args(["mkdir", "-p", host])
                        .output()
                        .await;
                    cmd.arg("-v").arg(format!("{}:{}", host, container));
                }
            }

            cmd.arg(image);

            let output = cmd.output().await.context("Failed to create container")?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(anyhow::anyhow!("Failed to create container: {}", stderr));
            }
        } else {
            let output = tokio::process::Command::new("sudo")
                .args(["podman", "start", app_id])
                .output()
                .await
                .context("Failed to start container")?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(anyhow::anyhow!("Failed to start container: {}", stderr));
            }
        }

        Ok(serde_json::json!({ "status": "started", "app_id": app_id }))
    }

    /// Stop a bundled app
    pub(super) async fn handle_bundled_app_stop(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let app_id = params
            .get("app_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;

        let output = tokio::process::Command::new("sudo")
            .args(["podman", "stop", app_id])
            .output()
            .await
            .context("Failed to stop container")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(anyhow::anyhow!("Failed to stop container: {}", stderr));
        }

        Ok(serde_json::json!({ "status": "stopped", "app_id": app_id }))
    }
}

/// Get all container names for an app (handles multi-container apps like mempool)
async fn get_containers_for_app(package_id: &str) -> Result<Vec<String>> {
    validate_app_id(package_id)?;
    let output = tokio::process::Command::new("sudo")
        .args(["podman", "ps", "-a", "--format", "{{.Names}}"])
        .output()
        .await
        .context("Failed to list containers")?;
    let stdout = String::from_utf8_lossy(&output.stdout);
    let all: Vec<&str> = stdout.lines().filter(|s| !s.is_empty()).collect();

    let patterns: Vec<String> = match package_id {
        "mempool" | "mempool-web" => {
            vec![
                "mempool-electrs".into(),
                "mempool-api".into(),
                "archy-mempool-api".into(),
                "archy-mempool-web".into(),
                "mempool".into(),
                "archy-mempool-db".into(),
                "mysql-mempool".into(),
            ]
        }
        "fedimint" => vec!["fedimint".into(), "fedimint-ui".into(), "archy-fedimint".into()],
        "immich" => vec![
            "immich_postgres".into(),
            "immich_redis".into(),
            "immich_server".into(),
        ],
        "penpot" | "penpot-frontend" => vec![
            "penpot-postgres".into(),
            "penpot-valkey".into(),
            "penpot-backend".into(),
            "penpot-exporter".into(),
            "penpot-frontend".into(),
        ],
        _ => vec![package_id.to_string(), format!("archy-{}", package_id)],
    };

    let mut result = Vec::new();
    for name in all {
        for pat in &patterns {
            if name == pat {
                result.push(name.to_string());
                break;
            }
        }
    }
    Ok(result)
}

/// Get data directories to clean for an app.
/// Caller must validate package_id before calling.
fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
    let base = "/var/lib/archipelago";
    match package_id {
        "mempool" | "mempool-web" => vec![
            format!("{}/mempool", base),
            format!("{}/mysql-mempool", base),
            format!("{}/mempool-electrs", base),
        ],
        "fedimint" => vec![format!("{}/fedimint", base)],
        "immich" => vec![
            format!("{}/immich", base),
            format!("{}/immich-db", base),
        ],
        "penpot" | "penpot-frontend" => vec![
            format!("{}/penpot-assets", base),
            format!("{}/penpot-postgres", base),
        ],
        _ => vec![format!("{}/{}", base, package_id)],
    }
}

/// Trusted Docker registries. Only images from these sources are allowed.
const TRUSTED_REGISTRIES: &[&str] = &[
    "docker.io/",
    "ghcr.io/",
    "localhost/",
];

/// Validate Docker image against trusted registry allowlist.
/// Detect which Bitcoin container is running on archy-net for DNS resolution.
/// Returns the container name to use as the RPC host (e.g., "bitcoin-knots").
fn detect_bitcoin_container_name() -> String {
    // Synchronous check — called from get_app_config which is sync
    let output = std::process::Command::new("sudo")
        .args(["podman", "ps", "--format", "{{.Names}}"])
        .output();
    if let Ok(out) = output {
        let names = String::from_utf8_lossy(&out.stdout);
        for candidate in &["bitcoin-knots", "bitcoin-core", "bitcoin"] {
            if names.lines().any(|l| l.trim() == *candidate) {
                return candidate.to_string();
            }
        }
    }
    // Default to bitcoin-knots (most common)
    "bitcoin-knots".to_string()
}

fn is_valid_docker_image(image: &str) -> bool {
    if image.is_empty() || image.len() > 256 {
        return false;
    }
    // Reject shell metacharacters
    let dangerous_chars = ['&', '|', ';', '`', '$', '(', ')', '<', '>', '\n', '\r'];
    if image.chars().any(|c| dangerous_chars.contains(&c)) {
        return false;
    }
    // Must come from a trusted registry
    TRUSTED_REGISTRIES.iter().any(|r| image.starts_with(r))
}

/// Validate that a package/app ID is safe (lowercase alphanumeric + hyphens, 1-64 chars).
fn validate_app_id(id: &str) -> Result<()> {
    if id.is_empty() || id.len() > 64 {
        anyhow::bail!("Invalid app id: must be 1-64 characters");
    }
    if !id.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') {
        anyhow::bail!("Invalid app id: only lowercase letters, digits, and hyphens allowed");
    }
    if id.starts_with('-') {
        anyhow::bail!("Invalid app id: must not start with a hyphen");
    }
    Ok(())
}

/// Per-app Linux capabilities needed beyond the default cap-drop=ALL.
/// Most apps need CHOWN/SETUID/SETGID for internal user switching.
fn get_app_capabilities(app_id: &str) -> Vec<String> {
    match app_id {
        // Apps that need user switching and file ownership changes
        "nextcloud" | "homeassistant" | "home-assistant" | "btcpay-server" | "btcpayserver"
        | "jellyfin" | "onlyoffice" | "onlyoffice-documentserver" | "portainer" => vec![
            "--cap-add=CHOWN".to_string(),
            "--cap-add=SETUID".to_string(),
            "--cap-add=SETGID".to_string(),
            "--cap-add=DAC_OVERRIDE".to_string(),
        ],
        // Nginx Proxy Manager needs to bind low ports
        "nginx-proxy-manager" => vec![
            "--cap-add=CHOWN".to_string(),
            "--cap-add=SETUID".to_string(),
            "--cap-add=SETGID".to_string(),
            "--cap-add=NET_BIND_SERVICE".to_string(),
        ],
        // Bitcoin and Lightning need file ownership ops
        "bitcoin" | "bitcoin-core" | "bitcoin-knots" | "lnd" | "fedimint" => vec![
            "--cap-add=CHOWN".to_string(),
            "--cap-add=SETUID".to_string(),
            "--cap-add=SETGID".to_string(),
        ],
        // Grafana runs as specific UID (472)
        "grafana" => vec![
            "--cap-add=CHOWN".to_string(),
            "--cap-add=SETUID".to_string(),
            "--cap-add=SETGID".to_string(),
        ],
        // Minimal apps (searxng, filebrowser, uptime-kuma, etc.) need no extra caps
        _ => vec![],
    }
}

/// Apps safe to run with --read-only root filesystem.
/// These work correctly with volume mounts + tmpfs for /tmp and /run.
fn is_readonly_compatible(app_id: &str) -> bool {
    matches!(
        app_id,
        "searxng" | "grafana" | "uptime-kuma" | "filebrowser" | "photoprism" | "vaultwarden"
    )
}

/// Get app-specific configuration
/// Returns: (ports, volumes, env_vars, custom_command, custom_args)
fn get_app_config(
    app_id: &str,
    host_ip: &str,
    allocator: &mut PortAllocator,
) -> (Vec<String>, Vec<String>, Vec<String>, Option<String>, Option<Vec<String>>) {
    match app_id {
        "homeassistant" | "home-assistant" => (
            vec!["8123:8123".to_string()],
            vec!["/var/lib/archipelago/home-assistant:/config".to_string()],
            vec!["TZ=UTC".to_string()],
            None,
            None,
        ),
        "bitcoin" | "bitcoin-core" | "bitcoin-knots" => (
            vec!["8332:8332".to_string(), "8333:8333".to_string()],
            vec!["/var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin".to_string()],
            vec![],
            None,
            None,
        ),
        "lnd" => (
            vec!["9735:9735".to_string(), "10009:10009".to_string(), "8080:8080".to_string()],
            vec!["/var/lib/archipelago/lnd:/root/.lnd".to_string()],
            vec!["BITCOIN_ACTIVE=1".to_string()],
            None,
            None,
        ),
        "btcpay-server" | "btcpayserver" => (
            vec!["23000:49392".to_string()],
            vec!["/var/lib/archipelago/btcpay:/datadir".to_string()],
            vec![
                "ASPNETCORE_URLS=http://0.0.0.0:49392".to_string(),
                "BTCPAY_PROTOCOL=http".to_string(),
                format!("BTCPAY_HOST={}:23000", host_ip),
                "BTCPAY_CHAINS=btc".to_string(),
                format!("BTCPAY_BTCRPCURL=http://{}:8332", host_ip),
                "BTCPAY_BTCRPCUSER=archipelago".to_string(),
                "BTCPAY_BTCRPCPASSWORD=archipelago123".to_string(),
                "BTCPAY_POSTGRES=User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true".to_string(),
            ],
            None,
            None,
        ),
        "mempool" | "mempool-web" => (
            vec!["4080:8080".to_string()],
            vec![],
            vec![format!("BACKEND_MAINNET_HTTP_HOST={}", host_ip)],
            None,
            None,
        ),
        "mempool-api" => (
            vec!["8999:8999".to_string()],
            vec!["/var/lib/archipelago/mempool:/data".to_string()],
            vec![
                "MEMPOOL_BACKEND=electrum".to_string(),
                "ELECTRUM_HOST=mempool-electrs".to_string(),
                "ELECTRUM_PORT=50001".to_string(),
                "ELECTRUM_TLS_ENABLED=false".to_string(),
                format!("CORE_RPC_HOST={}", host_ip),
                "CORE_RPC_PORT=8332".to_string(),
                "CORE_RPC_USERNAME=archipelago".to_string(),
                "CORE_RPC_PASSWORD=archipelago123".to_string(),
                "DATABASE_ENABLED=true".to_string(),
                "DATABASE_HOST=archy-mempool-db".to_string(),
                "DATABASE_DATABASE=mempool".to_string(),
                "DATABASE_USERNAME=mempool".to_string(),
                "DATABASE_PASSWORD=mempoolpass".to_string(),
            ],
            None,
            None,
        ),
        "mempool-electrs" | "electrs" => {
            // Detect which bitcoin container is running for archy-net DNS resolution
            let bitcoin_host = detect_bitcoin_container_name();
            (
                vec!["50001:50001".to_string()],
                vec!["/var/lib/archipelago/mempool-electrs:/data".to_string()],
                vec![],
                None,
                Some(vec![
                    "--daemon-rpc-addr".to_string(),
                    format!("{}:8332", bitcoin_host),
                    "--cookie".to_string(),
                    "archipelago:archipelago123".to_string(),
                    "--jsonrpc-import".to_string(),
                    "--electrum-rpc-addr".to_string(),
                    "0.0.0.0:50001".to_string(),
                    "--db-dir".to_string(),
                    "/data".to_string(),
                    "--lightmode".to_string(),
                ]),
            )
        },
        "mysql-mempool" => (
            vec![],
            vec!["/var/lib/archipelago/mysql-mempool:/var/lib/mysql".to_string()],
            vec![
                "MYSQL_DATABASE=mempool".to_string(),
                "MYSQL_USER=mempool".to_string(),
                "MYSQL_PASSWORD=mempoolpass".to_string(),
                "MYSQL_ROOT_PASSWORD=rootpass".to_string(),
            ],
            None,
            None,
        ),
        "grafana" => (
            vec!["3000:3000".to_string()],
            vec!["/var/lib/archipelago/grafana:/var/lib/grafana".to_string()],
            vec!["GF_PATHS_DATA=/var/lib/grafana".to_string(), "GF_USERS_ALLOW_SIGN_UP=false".to_string()],
            None,
            None,
        ),
        "searxng" => (
            vec!["8888:8080".to_string()],
            vec![],
            vec![],
            None,
            None,
        ),
        "ollama" => (
            vec!["11434:11434".to_string()],
            vec!["/var/lib/archipelago/ollama:/root/.ollama".to_string()],
            vec![],
            None,
            None,
        ),
        "onlyoffice" | "onlyoffice-documentserver" => (
            vec!["9980:80".to_string()],
            vec![],
            vec![],
            None,
            None,
        ),
        "penpot" | "penpot-frontend" => (
            vec!["9001:80".to_string()],
            vec![],
            vec![],
            None,
            None,
        ),
        "nextcloud" => {
            let host_port = allocator
                .allocate_or_get(app_id, 8085, 80)
                .unwrap_or(8085);
            (
                vec![format!("{}:80", host_port)],
                vec!["/var/lib/archipelago/nextcloud:/var/www/html".to_string()],
                vec![],
                None,
                None,
            )
        }
        "vaultwarden" => {
            let host_port = allocator
                .allocate_or_get(app_id, 8082, 80)
                .unwrap_or(8082);
            (
                vec![format!("{}:80", host_port)],
                vec!["/var/lib/archipelago/vaultwarden:/data".to_string()],
                vec![],
                None,
                None,
            )
        }
        "jellyfin" => (
            vec!["8096:8096".to_string()],
            vec!["/var/lib/archipelago/jellyfin/config:/config".to_string(), "/var/lib/archipelago/jellyfin/cache:/cache".to_string()],
            vec![],
            None,
            None,
        ),
        "photoprism" => (
            vec!["2342:2342".to_string()],
            vec!["/var/lib/archipelago/photoprism:/photoprism/storage".to_string()],
            vec!["PHOTOPRISM_ADMIN_PASSWORD=archipelago".to_string(), "PHOTOPRISM_DEFAULT_LOCALE=en".to_string()],
            None,
            None,
        ),
        "immich" => (
            vec!["2283:2283".to_string()],
            vec!["/var/lib/archipelago/immich:/usr/src/app/upload".to_string()],
            vec![
                "DB_HOSTNAME=immich_postgres".to_string(),
                "DB_USERNAME=postgres".to_string(),
                "DB_PASSWORD=immichpass".to_string(),
                "DB_DATABASE_NAME=immich".to_string(),
                "REDIS_HOSTNAME=immich_redis".to_string(),
                "UPLOAD_LOCATION=/usr/src/app/upload".to_string(),
            ],
            None,
            None,
        ),
        "filebrowser" => {
            let host_port = allocator
                .allocate_or_get(app_id, 8083, 80)
                .unwrap_or(8083);
            (
                vec![format!("{}:80", host_port)],
                vec!["/var/lib/archipelago/filebrowser:/srv".to_string()],
                vec![],
                None,
                None,
            )
        }
        "nginx-proxy-manager" => (
            vec!["81:81".to_string(), "8084:80".to_string(), "8443:443".to_string()],
            vec![
                "/var/lib/archipelago/nginx-proxy-manager/data:/data".to_string(),
                "/var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt".to_string(),
            ],
            vec![],
            None,
            None,
        ),
        "portainer" => (
            vec!["9000:9000".to_string()],
            vec!["/var/lib/archipelago/portainer:/data".to_string(), "/var/run/podman/podman.sock:/var/run/docker.sock".to_string()],
            vec![],
            None,
            None,
        ),
        "uptime-kuma" => (
            vec!["3001:3001".to_string()],
            vec!["/var/lib/archipelago/uptime-kuma:/app/data".to_string()],
            vec!["TZ=UTC".to_string()],
            None,
            None,
        ),
        "tailscale" => (
            vec!["8240:8240".to_string()],
            vec![
                "/var/lib/archipelago/tailscale:/var/lib/tailscale".to_string(),
            ],
            vec![
                "TS_STATE_DIR=/var/lib/tailscale".to_string(),
            ],
            Some("sh -c 'tailscale web --listen 0.0.0.0:8240 & exec tailscaled'".to_string()),
            None,
        ),
        "fedimint" => (
            vec![
                "8173:8173".to_string(),
                "8174:8174".to_string(),
                "8175:8175".to_string(),
            ],
            vec!["/var/lib/archipelago/fedimint:/data".to_string()],
            vec![
                "FM_DATA_DIR=/data".to_string(),
                "FM_BITCOIND_USERNAME=archipelago".to_string(),
                "FM_BITCOIND_PASSWORD=archipelago123".to_string(),
                "FM_BITCOIN_NETWORK=bitcoin".to_string(),
                "FM_BIND_P2P=0.0.0.0:8173".to_string(),
                "FM_BIND_API=0.0.0.0:8174".to_string(),
                "FM_BIND_UI=0.0.0.0:8175".to_string(),
                format!("FM_P2P_URL=fedimint://{}:8173", host_ip),
                format!("FM_API_URL=ws://{}:8174", host_ip),
                format!("FM_BITCOIND_URL=http://{}:8332", host_ip),
            ],
            None,
            None,
        ),
        "indeedhub" => (
            vec!["7777:7777".to_string()],
            vec![],
            vec!["NGINX_HOST=0.0.0.0".to_string(), "NGINX_PORT=7777".to_string()],
            None,
            None,
        ),
        _ => (vec![], vec![], vec![], None, None),
    }
}
