use crate::config::CliConfig;
use crate::project_config::ProjectConfig;
use std::path::Path;
use std::process::Command;
use std::time::Duration;

const CLI_DEPLOY_API_VERSION: u32 = 1;

pub async fn run(
    allow_destructive: bool,
    commit_hash_override: Option<String>,
    env: Option<&str>,
    tenant_override: Option<&str>,
) -> eyre::Result<()> {
    let mut config = CliConfig::load_with_env(env)?;
    if let Some(tenant) = tenant_override {
        config.tenant_override = Some(tenant.to_string());
    }

    if let Some(ref name) = config.env_name {
        println!("Environment: {}", name);
    }

    // Check if this is a web project
    let project = ProjectConfig::find_and_load()?;
    let mode = project
        .as_ref()
        .and_then(|p| p.service.mode.as_deref())
        .unwrap_or("rust");

    let commit_hash = commit_hash_override.or_else(detect_git_commit_hash);

    if mode == "web" {
        let service_name = project
            .as_ref()
            .and_then(|p| p.service.name.as_deref())
            .ok_or_else(|| eyre::eyre!("Web mode requires [service].name in Cufflink.toml"))?;
        deploy_web(
            &config,
            service_name,
            project.as_ref(),
            commit_hash.as_deref(),
            env,
        )
        .await
    } else {
        deploy_rust(
            &config,
            allow_destructive,
            project.as_ref(),
            commit_hash.as_deref(),
            env,
        )
        .await
    }
}

async fn deploy_rust(
    config: &CliConfig,
    allow_destructive: bool,
    project: Option<&ProjectConfig>,
    commit_hash: Option<&str>,
    env: Option<&str>,
) -> eyre::Result<()> {
    let client = config.http_client();

    verify_platform_version(config, &client).await?;

    println!("Building service...");
    let output = Command::new("cargo")
        .args(["run", "--", "--emit-manifest"])
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eyre::bail!("Build failed:\n{}", stderr);
    }

    let stdout = String::from_utf8(output.stdout)?;
    let manifest: serde_json::Value = serde_json::from_str(stdout.trim())
        .map_err(|e| eyre::eyre!("Failed to parse manifest JSON: {}", e))?;

    let service_name = manifest["name"].as_str().unwrap_or("unknown");
    let mode = manifest["mode"].as_str().unwrap_or("crud");
    println!("Deploying {} (mode: {})...", service_name, mode);

    let wasm_artifact = if mode == "wasm" {
        let artifact = build_wasm_artifact()?;
        println!(
            "  Bundled WASM: {} bytes, hash: {}",
            artifact.size,
            &artifact.hash[..16]
        );
        Some(artifact)
    } else {
        None
    };

    let expected_wasm_hash = wasm_artifact.as_ref().map(|a| a.hash.clone());

    let deploy_req = serde_json::json!({
        "manifest": manifest,
        "allow_destructive": allow_destructive,
        "commit_hash": commit_hash,
        "wasm_base64": wasm_artifact.as_ref().map(|a| a.base64.clone()),
        "cli_version": env!("CARGO_PKG_VERSION"),
        "api_version": CLI_DEPLOY_API_VERSION,
    });

    let resp = config
        .auth_request(
            &client,
            reqwest::Method::POST,
            &format!("{}/api/services/deploy", config.api_url),
        )
        .json(&deploy_req)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Deploy failed ({}): {}", status, body);
    }

    let body: serde_json::Value = resp.json().await?;
    let service_id = body["service_id"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("No service_id in deploy response"))?
        .to_string();

    if let Some(ref expected) = expected_wasm_hash {
        match body["wasm_hash"].as_str() {
            Some(h) if h == expected => {}
            Some(h) => {
                eyre::bail!(
                    "WASM hash mismatch: CLI built {} but platform stored {}. \
                     This indicates corruption somewhere in the upload path.",
                    &expected[..16],
                    &h[..16.min(h.len())]
                );
            }
            None => {
                if body["skipped"].as_bool() != Some(true) {
                    eyre::bail!(
                        "Platform did not echo wasm_hash in the deploy response. \
                         This usually means the platform is older than expected; \
                         check `curl {}/api/version`.",
                        config.api_url
                    );
                }
            }
        }
    }

    maybe_sync_configs(config, &service_id, project, env).await?;

    if body["skipped"].as_bool() == Some(true) {
        println!(
            "No schema changes for {} v{} — manifest unchanged",
            body["name"].as_str().unwrap_or("?"),
            body["version"]
        );
        if mode == "wasm" {
            upload_wasm(config, &client, &service_id).await?;
        }
        verify_deploy_ready(config, &client, &service_id).await?;
        return Ok(());
    }

    println!(
        "Deployed {} v{}",
        body["name"].as_str().unwrap_or("?"),
        body["version"]
    );
    println!("  Schema changes: {}", body["schema_changes"]);
    println!("  Deployment ID:  {}", body["deployment_id"]);
    println!("  Tenant:         {}", body["tenant_slug"]);
    if let Some(hash) = body["commit_hash"].as_str() {
        println!("  Commit:         {}", hash);
    }
    if let Some(hash) = body["wasm_hash"].as_str() {
        println!("  WASM hash:      {}", &hash[..16.min(hash.len())]);
    }
    if manifest["on_migrate"].is_string() {
        println!(
            "  on_migrate:     {} (ran between phases)",
            manifest["on_migrate"].as_str().unwrap_or("?")
        );
    }

    verify_deploy_ready(config, &client, &service_id).await?;

    Ok(())
}

async fn verify_platform_version(config: &CliConfig, client: &reqwest::Client) -> eyre::Result<()> {
    let url = format!("{}/api/version", config.api_url);
    let resp = match client.get(&url).send().await {
        Ok(r) => r,
        Err(e) => {
            eyre::bail!(
                "Could not reach platform at {}: {}\n\
                 Check your CUFFLINK_API_URL / Cufflink.toml api_url and network connectivity.",
                url,
                e
            );
        }
    };

    if resp.status().as_u16() == 404 {
        // Old platform that predates `/api/version`. Refuse — we have no
        // way to know whether it understands bundled-WASM or not, and
        // silently sending fields it drops is exactly the failure mode
        // we're fixing.
        eyre::bail!(
            "Platform at {} does not expose /api/version. \
             This CLI (v{}) requires a platform that supports deploy api_version >= {}. \
             Upgrade the platform image before deploying.",
            config.api_url,
            env!("CARGO_PKG_VERSION"),
            CLI_DEPLOY_API_VERSION
        );
    }

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Platform /api/version returned {}: {}", status, body);
    }

    let info: serde_json::Value = resp.json().await?;
    let platform_api = info["api_version"].as_u64().unwrap_or(0) as u32;
    if platform_api < CLI_DEPLOY_API_VERSION {
        eyre::bail!(
            "Platform deploy api_version is {} but this CLI (v{}) requires >= {}. \
             Upgrade the platform image before deploying.",
            platform_api,
            env!("CARGO_PKG_VERSION"),
            CLI_DEPLOY_API_VERSION
        );
    }
    Ok(())
}

/// Poll `wasm/status` until `db_state: true` (binary durable in S3).
/// No-op for non-WASM services.
async fn verify_deploy_ready(
    config: &CliConfig,
    client: &reqwest::Client,
    service_id: &str,
) -> eyre::Result<()> {
    let url = format!("{}/api/services/{}/wasm/status", config.api_url, service_id);

    let delays = [
        Duration::from_millis(100),
        Duration::from_millis(200),
        Duration::from_millis(500),
        Duration::from_secs(1),
        Duration::from_secs(2),
        Duration::from_secs(4),
        Duration::from_secs(8),
        Duration::from_secs(14),
    ];

    let mut last_body = String::new();
    for delay in delays {
        let resp = config
            .auth_request(client, reqwest::Method::GET, &url)
            .send()
            .await?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            eyre::bail!(
                "Deploy verification failed ({} on wasm/status): {}",
                status,
                body
            );
        }
        let body: serde_json::Value = resp.json().await?;
        last_body = serde_json::to_string(&body).unwrap_or_default();

        let is_wasm_service = body["wasm_hash"]
            .as_str()
            .map(|s| !s.is_empty())
            .unwrap_or(false);
        if !is_wasm_service {
            return Ok(());
        }

        if body["db_state"].as_bool() == Some(true) {
            println!(
                "  Deploy verified: db_state=true, local_cache={}",
                body["local_cache"].as_bool().unwrap_or(false)
            );
            return Ok(());
        }

        tokio::time::sleep(delay).await;
    }

    eyre::bail!(
        "Deploy verification timed out: wasm/status never reported db_state=true \
         within 30s. Last status body: {}. The platform accepted the deploy but \
         the WASM binary is not durably persisted — this is a bug. Check platform \
         logs (`cufflink_deployment_wasm_missing_total` metric) and retry.",
        last_body
    );
}

/// Deploy a Next.js web app: run checks, build, create tarball, upload
async fn deploy_web(
    config: &CliConfig,
    service_name: &str,
    project: Option<&ProjectConfig>,
    commit_hash: Option<&str>,
    env: Option<&str>,
) -> eyre::Result<()> {
    println!("Deploying web app: {}", service_name);

    // Step 1: Pre-deploy checks
    let pkg_json = read_package_json()?;
    let scripts = pkg_json["scripts"].as_object();

    if let Some(s) = scripts {
        if s.contains_key("typecheck") {
            run_npm_script("typecheck", "Type checking")?;
        }
        if s.contains_key("lint") {
            run_npm_script("lint", "Linting")?;
        }
        if s.contains_key("test") {
            run_npm_script("test", "Running tests")?;
        }
    }

    // Step 2: Register service on platform (POST manifest before build so service exists)
    let version = pkg_json["version"].as_str().unwrap_or("0.1.0");
    let manifest = serde_json::json!({
        "name": service_name,
        "mode": "web",
        "version": version,
        "tables": [],
    });

    let deploy_req = serde_json::json!({
        "manifest": manifest,
        "allow_destructive": false,
        "commit_hash": commit_hash,
    });

    let client = config.http_client();
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::POST,
            &format!("{}/api/services/deploy", config.api_url),
        )
        .json(&deploy_req)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Deploy failed ({}): {}", status, body);
    }

    let body: serde_json::Value = resp.json().await?;
    let service_id = body["service_id"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("No service_id in deploy response"))?;

    println!(
        "Registered {} v{}",
        body["name"].as_str().unwrap_or("?"),
        body["version"]
    );
    println!("  Deployment ID: {}", body["deployment_id"]);
    println!("  Tenant:        {}", body["tenant_slug"]);
    if let Some(hash) = body["commit_hash"].as_str() {
        println!("  Commit:        {}", hash);
    }

    // Step 3: Sync configs from Cufflink.toml to platform
    maybe_sync_configs(config, service_id, project, env).await?;

    // Step 4: Fetch all configs (including newly synced) for build-time injection
    println!("Fetching service configs...");
    let build_envs = fetch_service_configs(config, service_name).await;
    if !build_envs.is_empty() {
        println!(
            "  Injecting {} service configs into build",
            build_envs.len()
        );
    }

    // Step 5: Check pnpm workspace membership before building
    check_pnpm_workspace()?;

    // Step 6: Build
    println!("Building Next.js app...");
    let pm = detect_package_manager();
    let mut build_cmd = Command::new(&pm);
    build_cmd.args(["run", "build"]);
    for (key, value) in &build_envs {
        build_cmd.env(key, value);
    }
    let build_output = build_cmd.output()?;

    if !build_output.status.success() {
        let stderr = String::from_utf8_lossy(&build_output.stderr);
        let stdout = String::from_utf8_lossy(&build_output.stdout);
        eyre::bail!("Build failed:\n{}\n{}", stdout, stderr);
    }
    println!("  Build complete");

    // Step 6: Verify build output exists
    if !Path::new(".next").exists() {
        eyre::bail!("Next.js build output not found at .next/");
    }

    // Step 7: Create tarball
    println!("Creating deployment artifact...");
    let tarball = create_web_tarball()?;
    let tarball_size = tarball.len();
    println!(
        "  Artifact size: {} bytes ({:.1} MB)",
        tarball_size,
        tarball_size as f64 / 1_048_576.0
    );

    // Step 8: Upload tarball
    println!("Uploading web artifact...");
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::POST,
            &format!("{}/api/services/{}/web", config.api_url, service_id),
        )
        .header("Content-Type", "application/gzip")
        .body(tarball)
        .send()
        .await?;

    if resp.status().is_success() {
        let body: serde_json::Value = resp.json().await?;
        println!(
            "  Artifact hash: {}",
            body["artifact_hash"].as_str().unwrap_or("?")
        );
        println!("  Size:          {} bytes", body["size_bytes"]);
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Web artifact upload failed ({}): {}", status, body);
    }

    println!("\nWeb app deployed successfully!");
    Ok(())
}

fn check_pnpm_workspace() -> eyre::Result<()> {
    use crate::pnpm::{check_workspace_membership, WorkspaceCheck};

    let cwd = std::env::current_dir()?;
    if let WorkspaceCheck::NotIncluded {
        workspace_file,
        suggested_glob,
        ..
    } = check_workspace_membership(&cwd)
    {
        eyre::bail!(
            "This package is inside a pnpm workspace but is not listed in {}\n\n\
             pnpm install will not have installed dependencies for this package,\n\
             so the build will fail.\n\n\
             Add \"{}\" to the packages list in {} and run pnpm install",
            workspace_file.display(),
            suggested_glob,
            workspace_file.display(),
        );
    }
    Ok(())
}

fn read_package_json() -> eyre::Result<serde_json::Value> {
    let content = std::fs::read_to_string("package.json")
        .map_err(|_| eyre::eyre!("package.json not found in current directory"))?;
    serde_json::from_str(&content).map_err(|e| eyre::eyre!("Failed to parse package.json: {}", e))
}

fn run_npm_script(script: &str, label: &str) -> eyre::Result<()> {
    println!("{}...", label);
    let pm = detect_package_manager();
    let output = Command::new(&pm).args(["run", script]).output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);
        eyre::bail!("{} failed:\n{}\n{}", label, stdout, stderr);
    }
    println!("  {} passed", label);
    Ok(())
}

/// Detect the package manager by searching for lock files upward from CWD.
fn detect_package_manager() -> String {
    let mut dir = std::env::current_dir().unwrap_or_default();
    loop {
        if dir.join("pnpm-lock.yaml").exists() {
            return "pnpm".to_string();
        }
        if dir.join("yarn.lock").exists() {
            return "yarn".to_string();
        }
        if dir.join("bun.lockb").exists() || dir.join("bun.lock").exists() {
            return "bun".to_string();
        }
        if dir.join("package-lock.json").exists() {
            return "npm".to_string();
        }
        if !dir.pop() {
            break;
        }
    }
    "npm".to_string()
}

/// Create a gzipped tarball of the Next.js app for deployment.
/// Bundles .next/, node_modules/, package.json, and public/ — same approach
/// as a standard Next.js Docker deployment (no standalone mode needed).
///
/// Uses the system `tar` command instead of the Rust tar crate because the
/// crate fails to archive files cloned by pnpm on macOS APFS filesystems.
fn create_web_tarball() -> eyre::Result<Vec<u8>> {
    let cwd = std::env::current_dir()?;
    let nm_dir = resolve_node_modules()?;

    // Create a staging dir with symlinks so tar can bundle files from
    // different locations (CWD and the pnpm deploy temp dir) into one archive.
    let staging = tempfile::tempdir()?;
    let stage = staging.path();

    // Symlink .next/ from CWD
    let next_dir = cwd.join(".next");
    if next_dir.exists() {
        std::os::unix::fs::symlink(&next_dir, stage.join(".next"))?;
    }

    // Symlink node_modules/ from pnpm deploy output (or nearest)
    std::os::unix::fs::symlink(&nm_dir, stage.join("node_modules"))?;

    // Symlink package.json from CWD
    std::os::unix::fs::symlink(cwd.join("package.json"), stage.join("package.json"))?;

    // Symlink public/ from CWD if it exists
    let public_dir = cwd.join("public");
    let has_public = public_dir.exists();
    if has_public {
        std::os::unix::fs::symlink(&public_dir, stage.join("public"))?;
    }

    // Use system tar with -h to dereference symlinks
    let mut args = vec!["czfh", "-", ".next", "node_modules", "package.json"];
    if has_public {
        args.push("public");
    }

    let output = Command::new("tar")
        .args(&args)
        .current_dir(stage)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eyre::bail!("tar command failed: {}", stderr);
    }

    // Clean up pnpm deploy temp dir
    if let Some(parent) = nm_dir.parent() {
        if parent.starts_with(std::env::temp_dir()) {
            let _ = std::fs::remove_dir_all(parent);
        }
    }

    Ok(output.stdout)
}

/// Resolve the node_modules directory to bundle.
/// In pnpm monorepos, uses `pnpm deploy --prod` to create a pruned, flat
/// node_modules with only production dependencies. Otherwise falls back to
/// finding the nearest node_modules directory.
fn resolve_node_modules() -> eyre::Result<std::path::PathBuf> {
    // Check if we're in a pnpm monorepo
    if let Some(root) = find_pnpm_workspace_root() {
        println!("  Detected pnpm monorepo, creating pruned node_modules...");
        let temp_dir = tempfile::tempdir()?;
        let deploy_dir = temp_dir.keep();

        let pkg_json: serde_json::Value =
            serde_json::from_str(&std::fs::read_to_string("package.json")?)?;
        let pkg_name = pkg_json["name"]
            .as_str()
            .ok_or_else(|| eyre::eyre!("package.json missing 'name' field"))?;

        let output = Command::new("pnpm")
            .args([
                "--filter",
                pkg_name,
                "deploy",
                "--prod",
                &deploy_dir.display().to_string(),
            ])
            .current_dir(&root)
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            // Fall back to find_node_modules if pnpm deploy fails
            eprintln!(
                "  pnpm deploy failed, falling back to nearest node_modules: {}",
                stderr
            );
            let _ = std::fs::remove_dir_all(&deploy_dir);
            return find_node_modules();
        }

        let nm = deploy_dir.join("node_modules");
        if nm.is_dir() {
            println!("  Pruned node_modules ready");
            return Ok(nm);
        }

        let _ = std::fs::remove_dir_all(&deploy_dir);
    }

    find_node_modules()
}

/// Find the pnpm workspace root by searching upward for pnpm-lock.yaml.
fn find_pnpm_workspace_root() -> Option<std::path::PathBuf> {
    let mut dir = std::env::current_dir().ok()?;
    loop {
        if dir.join("pnpm-lock.yaml").exists() {
            // Only consider it a monorepo if we're not already at the root
            // (i.e., there's a parent with a different package.json)
            return Some(dir);
        }
        if !dir.pop() {
            break;
        }
    }
    None
}

/// Find the nearest node_modules directory by searching upward from CWD.
/// Skips directories that only contain pnpm metadata (.pnpm, .modules.yaml)
/// without actual packages — common in pnpm monorepo workspace subdirectories.
fn find_node_modules() -> eyre::Result<std::path::PathBuf> {
    let mut dir = std::env::current_dir()?;
    loop {
        let candidate = dir.join("node_modules");
        if candidate.is_dir() && has_packages(&candidate) {
            return Ok(candidate);
        }
        if !dir.pop() {
            break;
        }
    }
    eyre::bail!("node_modules not found in any parent directory")
}

/// Check that a node_modules directory has actual packages, not just pnpm metadata.
fn has_packages(node_modules: &Path) -> bool {
    let Ok(entries) = std::fs::read_dir(node_modules) else {
        return false;
    };
    entries.filter_map(|e| e.ok()).any(|e| {
        let name = e.file_name();
        let name = name.to_string_lossy();
        !name.starts_with('.')
    })
}

/// Sync configs and secrets from Cufflink.toml to the platform, if any are defined.
async fn maybe_sync_configs(
    config: &CliConfig,
    service_id: &str,
    project: Option<&ProjectConfig>,
    env: Option<&str>,
) -> eyre::Result<()> {
    let project = match project {
        Some(p) => p,
        None => return Ok(()),
    };

    let env_name = env
        .map(|s| s.to_string())
        .or_else(|| project.service.default_env.clone());

    let env_name = match env_name {
        Some(n) => n,
        None => return Ok(()),
    };

    let env_config = match project.environments.get(&env_name) {
        Some(c) => c,
        None => return Ok(()),
    };

    if env_config.config.is_empty() && env_config.secrets.is_empty() {
        return Ok(());
    }

    println!("Syncing configs...");
    super::config_cmd::sync_to_platform(
        config,
        service_id,
        &env_config.config,
        &env_config.secrets,
        project,
        &env_name,
    )
    .await
}

/// Fetch service configs from the platform to inject as build-time env vars.
/// Returns empty vec on any failure (first deploy, platform unreachable, etc.)
/// so the build proceeds with local env vars.
async fn fetch_service_configs(config: &CliConfig, service_name: &str) -> Vec<(String, String)> {
    let client = config.http_client();

    // Step 1: Find service ID by name
    let resp = match config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services", config.api_url),
        )
        .send()
        .await
    {
        Ok(r) => r,
        Err(_) => return Vec::new(),
    };

    let services: serde_json::Value = match resp.json().await {
        Ok(v) => v,
        Err(_) => return Vec::new(),
    };

    let service_id = match services["services"].as_array().and_then(|arr| {
        arr.iter()
            .find(|s| s["name"].as_str() == Some(service_name))
            .and_then(|s| s["id"].as_str())
    }) {
        Some(id) => id.to_string(),
        None => return Vec::new(),
    };

    // Step 2: Fetch configs for this service
    let resp = match config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services/{}/config", config.api_url, service_id),
        )
        .send()
        .await
    {
        Ok(r) if r.status().is_success() => r,
        _ => return Vec::new(),
    };

    let body: serde_json::Value = match resp.json().await {
        Ok(v) => v,
        Err(_) => return Vec::new(),
    };

    // Step 3: Extract key/value pairs
    body["configs"]
        .as_array()
        .map(|configs| {
            configs
                .iter()
                .filter_map(|c| {
                    let key = c["key"].as_str()?;
                    let value = c["value"].as_str()?;
                    Some((key.to_string(), value.to_string()))
                })
                .collect()
        })
        .unwrap_or_default()
}

fn detect_git_commit_hash() -> Option<String> {
    Command::new("git")
        .args(["rev-parse", "HEAD"])
        .output()
        .ok()
        .filter(|o| o.status.success())
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

struct WasmArtifact {
    base64: String,
    size: usize,
    hash: String,
}

fn build_wasm_artifact() -> eyre::Result<WasmArtifact> {
    let rustup_check = Command::new("rustup")
        .args(["target", "list", "--installed"])
        .output()?;
    let installed_targets = String::from_utf8_lossy(&rustup_check.stdout);
    if !installed_targets.contains("wasm32-unknown-unknown") {
        eyre::bail!("WASM target not installed. Run:\n  rustup target add wasm32-unknown-unknown");
    }

    println!("Building WASM module...");
    let build_output = Command::new("cargo")
        .args([
            "build",
            "--lib",
            "--target",
            "wasm32-unknown-unknown",
            "--release",
        ])
        .output()?;

    if !build_output.status.success() {
        let stderr = String::from_utf8_lossy(&build_output.stderr);
        eyre::bail!("WASM build failed:\n{}", stderr);
    }

    let wasm_path = find_wasm_artifact()?;
    let wasm_bytes = std::fs::read(&wasm_path)?;
    let size = wasm_bytes.len();
    println!("  WASM artifact: {} ({} bytes)", wasm_path.display(), size);

    use sha2::{Digest, Sha256};
    let mut hasher = Sha256::new();
    hasher.update(&wasm_bytes);
    let hash = hex::encode(hasher.finalize());

    use base64::Engine as _;
    let base64 = base64::engine::general_purpose::STANDARD.encode(&wasm_bytes);

    Ok(WasmArtifact { base64, size, hash })
}

/// Legacy WASM upload via the dedicated `POST /api/services/:id/wasm`
/// endpoint. Kept for backwards compat with platforms that don't yet
/// support bundled WASM in the deploy request, and for the manifest-
/// unchanged path where the deploy endpoint short-circuits before any
/// WASM handling.
async fn upload_wasm(
    config: &CliConfig,
    client: &reqwest::Client,
    service_id: &str,
) -> eyre::Result<()> {
    let artifact = build_wasm_artifact()?;
    let expected_hash = artifact.hash.clone();
    let upload_req = serde_json::json!({ "wasm_base64": artifact.base64 });

    println!("Uploading WASM module...");
    let resp = config
        .auth_request(
            client,
            reqwest::Method::POST,
            &format!("{}/api/services/{}/wasm", config.api_url, service_id),
        )
        .json(&upload_req)
        .send()
        .await?;

    if resp.status().is_success() {
        let body: serde_json::Value = resp.json().await?;
        let echoed = body["wasm_hash"].as_str().unwrap_or("");
        println!("  WASM hash:  {}", echoed);
        println!("  Size:       {} bytes", body["size_bytes"]);
        if echoed != expected_hash {
            eyre::bail!(
                "WASM hash mismatch on upload: CLI built {} but platform stored {}",
                &expected_hash[..16],
                &echoed[..16.min(echoed.len())]
            );
        }
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("WASM upload failed ({}): {}", status, body);
    }

    Ok(())
}

fn find_wasm_artifact() -> eyre::Result<std::path::PathBuf> {
    // Use cargo metadata to find the target directory and crate name
    let metadata_output = Command::new("cargo")
        .args(["metadata", "--format-version", "1", "--no-deps"])
        .output()?;

    if !metadata_output.status.success() {
        eyre::bail!("Failed to run cargo metadata");
    }

    let metadata: serde_json::Value = serde_json::from_slice(&metadata_output.stdout)?;
    let target_dir = metadata["target_directory"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("Cannot determine target directory"))?;

    // Find the lib target name from the current package
    let cwd = std::env::current_dir()?;
    let cwd_str = cwd.to_string_lossy();

    let crate_name = metadata["packages"]
        .as_array()
        .and_then(|pkgs| {
            pkgs.iter().find(|p| {
                p["manifest_path"]
                    .as_str()
                    .map(|mp| mp.starts_with(cwd_str.as_ref()))
                    .unwrap_or(false)
            })
        })
        .and_then(|pkg| {
            // Look for a cdylib target first, then fall back to package name
            pkg["targets"]
                .as_array()
                .and_then(|targets| {
                    targets.iter().find(|t| {
                        t["crate_types"]
                            .as_array()
                            .map(|types| types.iter().any(|ct| ct.as_str() == Some("cdylib")))
                            .unwrap_or(false)
                    })
                })
                .and_then(|t| t["name"].as_str())
                .or_else(|| pkg["name"].as_str())
        })
        .unwrap_or("unknown");

    let wasm_name = crate_name.replace('-', "_");
    let wasm_path = std::path::PathBuf::from(target_dir)
        .join("wasm32-unknown-unknown")
        .join("release")
        .join(format!("{}.wasm", wasm_name));

    if wasm_path.exists() {
        return Ok(wasm_path);
    }

    eyre::bail!(
        "WASM artifact not found at {:?}.\n\
         Ensure your Cargo.toml has:\n  \
         [lib]\n  \
         crate-type = [\"cdylib\"]",
        wasm_path
    )
}
