use crate::config::CliConfig;
use crate::workspace_config::WorkspaceConfig;
use comfy_table::{presets::NOTHING, Table, TableComponent};
use std::path::{Path, PathBuf};
use std::time::Instant;

fn load_workspace() -> eyre::Result<(WorkspaceConfig, PathBuf)> {
    let (config, root) = WorkspaceConfig::find_and_load()?;
    config.validate_paths(&root)?;
    Ok((config, root))
}

fn load_config_from_workspace(
    root: &Path,
    ws: &WorkspaceConfig,
    env: Option<&str>,
) -> eyre::Result<CliConfig> {
    let first = ws
        .services
        .first()
        .ok_or_else(|| eyre::eyre!("No services in workspace"))?;
    let original_dir = std::env::current_dir()?;
    std::env::set_current_dir(root.join(&first.path))?;
    let config = CliConfig::load_with_env(env);
    std::env::set_current_dir(&original_dir)?;
    config
}

fn make_table(headers: Vec<&str>) -> Table {
    let mut table = Table::new();
    table.load_preset(NOTHING);
    table.set_style(TableComponent::HeaderLines, '-');
    table.set_style(TableComponent::MiddleHeaderIntersections, ' ');
    table.set_header(headers);
    table
}

pub async fn deploy(
    skip: &[String],
    env: Option<&str>,
    tenant_override: Option<&str>,
    parallel: bool,
) -> eyre::Result<()> {
    let (ws, root) = load_workspace()?;
    let env = ws.resolve_env(env);
    println!("Workspace: {}", ws.workspace.name);

    if let Some(name) = env {
        println!("Environment: {}", name);
    }

    let deploy_services = ws.deploy_list(env);
    let deploy_services: Vec<_> = deploy_services
        .into_iter()
        .filter(|s| !skip.contains(&s.name))
        .collect();

    if deploy_services.is_empty() {
        println!("Nothing to deploy.");
        return Ok(());
    }

    if !skip.is_empty() {
        println!("Skipping: {}", skip.join(", "));
    }

    let results = if parallel {
        println!(
            "\nDeploying {} services in parallel...",
            deploy_services.len()
        );
        deploy_parallel(&root, &deploy_services, env, tenant_override).await
    } else {
        deploy_sequential(&root, &deploy_services, env, tenant_override).await
    }?;

    println!();
    let mut table = make_table(vec!["SERVICE", "STATUS"]);
    let mut failures = 0;
    let deployed: Vec<String> = results
        .iter()
        .filter(|(_, r)| r.is_ok())
        .map(|(n, _)| n.clone())
        .collect();

    for (name, result) in &results {
        let status = match result {
            Ok(_) => "deployed",
            Err(_) => {
                failures += 1;
                "FAILED"
            }
        };
        table.add_row(vec![name.as_str(), status]);
    }
    println!("{table}");

    // Seed phase
    let seed_services = ws.seed_list(env);
    let seed_services: Vec<_> = seed_services
        .into_iter()
        .filter(|s| deployed.contains(&s.name))
        .collect();

    if !seed_services.is_empty() {
        println!();
        run_seed_phase(&root, &ws, &seed_services, env).await;
    }

    if failures > 0 {
        eyre::bail!("{} service(s) failed to deploy", failures);
    }

    Ok(())
}

async fn deploy_sequential(
    root: &Path,
    services: &[&crate::workspace_config::WorkspaceService],
    env: Option<&str>,
    tenant_override: Option<&str>,
) -> eyre::Result<Vec<(String, Result<(), String>)>> {
    let original_dir = std::env::current_dir()?;
    let mut results = Vec::new();

    for svc in services {
        let service_dir = root.join(&svc.path);
        println!("\nDeploying '{}'...", svc.name);
        std::env::set_current_dir(&service_dir)?;

        match super::deploy::run(false, None, env, tenant_override).await {
            Ok(_) => results.push((svc.name.clone(), Ok(()))),
            Err(e) => {
                let msg = format!("{}", e);
                eprintln!("  Failed: {}", msg);
                results.push((svc.name.clone(), Err(msg)));
            }
        }
    }

    std::env::set_current_dir(&original_dir)?;
    Ok(results)
}

async fn deploy_parallel(
    root: &Path,
    services: &[&crate::workspace_config::WorkspaceService],
    env: Option<&str>,
    tenant_override: Option<&str>,
) -> eyre::Result<Vec<(String, Result<(), String>)>> {
    let mut handles = Vec::new();

    for svc in services {
        let name = svc.name.clone();
        let service_dir = root.join(&svc.path);

        let mut args = vec!["deploy".to_string()];
        if let Some(e) = env {
            args.extend(["--env".to_string(), e.to_string()]);
        }
        if let Some(t) = tenant_override {
            args.extend(["--tenant".to_string(), t.to_string()]);
        }

        let handle = tokio::spawn(async move {
            let output = tokio::process::Command::new("cufflink")
                .args(&args)
                .current_dir(&service_dir)
                .output()
                .await;

            match output {
                Ok(o) if o.status.success() => (name, Ok(())),
                Ok(o) => {
                    let stderr = String::from_utf8_lossy(&o.stderr);
                    let stdout = String::from_utf8_lossy(&o.stdout);
                    let msg = if !stderr.is_empty() {
                        stderr.to_string()
                    } else {
                        stdout.to_string()
                    };
                    (name, Err(msg.trim().to_string()))
                }
                Err(e) => (name, Err(format!("Failed to spawn: {}", e))),
            }
        });

        handles.push(handle);
    }

    let mut results = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(result) => results.push(result),
            Err(e) => results.push(("?".to_string(), Err(format!("Task panicked: {}", e)))),
        }
    }

    Ok(results)
}

async fn run_seed_phase(
    root: &std::path::Path,
    ws: &WorkspaceConfig,
    seed_services: &[&crate::workspace_config::WorkspaceService],
    env: Option<&str>,
) {
    println!("Seeding...");

    let config = match load_config_from_workspace(root, ws, env) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("Failed to load config for seeding: {}", e);
            return;
        }
    };

    for svc in seed_services {
        let seed_path = match &svc.seed {
            Some(p) => root.join(p),
            None => continue,
        };

        let service_id = match find_service_id(&config, &svc.name).await {
            Ok(id) => id,
            Err(e) => {
                eprintln!("  {} — failed to resolve: {}", svc.name, e);
                continue;
            }
        };

        match run_seed(&config, &service_id, &seed_path).await {
            Ok(rows) => println!("  {} — {} rows seeded", svc.name, rows),
            Err(e) => eprintln!("  {} — seed failed: {}", svc.name, e),
        }
    }
}

async fn find_service_id(config: &CliConfig, service_name: &str) -> eyre::Result<String> {
    config.find_service_id(service_name).await
}

async fn run_seed(
    config: &CliConfig,
    service_id: &str,
    seed_path: &std::path::Path,
) -> eyre::Result<u64> {
    let data = std::fs::read_to_string(seed_path)?;
    let mut payload: serde_json::Value = serde_json::from_str(&data)?;

    if payload.get("tables").is_none() {
        payload = serde_json::json!({ "tables": payload });
    }

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

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

    let body: serde_json::Value = resp.json().await?;
    Ok(body["total_rows"].as_u64().unwrap_or(0))
}

pub async fn test(skip: &[String], run_all: bool) -> eyre::Result<()> {
    let (ws, root) = load_workspace()?;
    println!("Workspace: {}", ws.workspace.name);
    println!();

    let services: Vec<_> = ws
        .services
        .iter()
        .filter(|s| !skip.contains(&s.name))
        .collect();

    if services.is_empty() {
        println!("No services to test.");
        return Ok(());
    }

    let original_dir = std::env::current_dir()?;
    let mut results: Vec<(String, bool, String)> = Vec::new();

    for svc in &services {
        let service_dir = root.join(&svc.path);

        // Skip directories without Cargo.toml (shared libraries, web apps)
        if !service_dir.join("Cargo.toml").exists() {
            results.push((svc.name.clone(), true, "skipped".to_string()));
            continue;
        }

        std::env::set_current_dir(&service_dir)?;
        print!("Testing {}... ", svc.name);

        let start = Instant::now();
        let mut args = vec!["test"];
        if !run_all {
            args.push("--quiet");
        }

        let output = std::process::Command::new("cargo").args(&args).output();

        let elapsed = start.elapsed();
        let time_str = format!("{:.1}s", elapsed.as_secs_f64());

        match output {
            Ok(o) if o.status.success() => {
                println!("passed ({})", time_str);
                results.push((svc.name.clone(), true, time_str));
            }
            Ok(o) => {
                println!("FAILED ({})", time_str);
                if run_all {
                    let stderr = String::from_utf8_lossy(&o.stderr);
                    let stdout = String::from_utf8_lossy(&o.stdout);
                    if !stdout.is_empty() {
                        eprintln!("{}", stdout);
                    }
                    if !stderr.is_empty() {
                        eprintln!("{}", stderr);
                    }
                }
                results.push((svc.name.clone(), false, time_str));
            }
            Err(e) => {
                println!("ERROR");
                results.push((svc.name.clone(), false, format!("error: {}", e)));
            }
        }
    }

    std::env::set_current_dir(&original_dir)?;

    println!();
    let mut table = make_table(vec!["SERVICE", "RESULT", "TIME"]);

    let mut passed = 0;
    let mut failed = 0;
    let mut skipped = 0;

    for (name, success, time) in &results {
        if time == "skipped" {
            skipped += 1;
            table.add_row(vec![name.as_str(), "skipped", "-"]);
        } else if *success {
            passed += 1;
            table.add_row(vec![name.as_str(), "passed", time.as_str()]);
        } else {
            failed += 1;
            table.add_row(vec![name.as_str(), "FAILED", time.as_str()]);
        }
    }

    println!("{table}");
    println!();

    let mut summary = format!("{} passed", passed);
    if failed > 0 {
        summary.push_str(&format!(", {} failed", failed));
    }
    if skipped > 0 {
        summary.push_str(&format!(", {} skipped", skipped));
    }
    println!("{}", summary);

    if failed > 0 {
        eyre::bail!("{} service(s) failed tests", failed);
    }

    Ok(())
}

pub async fn status(env: Option<&str>) -> eyre::Result<()> {
    let (ws, root) = load_workspace()?;
    let env = ws.resolve_env(env);
    println!("Workspace: {}", ws.workspace.name);

    let config = load_config_from_workspace(&root, &ws, env)?;
    if let Some(ref name) = config.env_name {
        println!("Environment: {}", name);
    }
    println!();

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

    let body: serde_json::Value = resp.json().await?;
    let api_services = body["services"].as_array();

    let mut table = make_table(vec!["SERVICE", "VERSION", "STATUS", "MODE"]);

    for svc in &ws.services {
        let api_svc =
            api_services.and_then(|arr| arr.iter().find(|s| s["name"].as_str() == Some(&svc.name)));

        match api_svc {
            Some(s) => {
                let version = format!("v{}", s["current_version"]);
                let status = s["status"].as_str().unwrap_or("?");
                let mode = s["mode"].as_str().unwrap_or("?");
                table.add_row(vec![svc.name.as_str(), &version, status, mode]);
            }
            None => {
                table.add_row(vec![svc.name.as_str(), "-", "not deployed", "-"]);
            }
        }
    }

    println!("{table}");
    println!("\n{} service(s)", ws.services.len());

    Ok(())
}

pub async fn seed(env: Option<&str>) -> eyre::Result<()> {
    let (ws, root) = load_workspace()?;
    let env = ws.resolve_env(env);
    println!("Workspace: {}", ws.workspace.name);

    let seed_services = ws.seed_list(env);
    if seed_services.is_empty() {
        println!("No services configured for seeding in this environment.");
        return Ok(());
    }

    run_seed_phase(&root, &ws, &seed_services, env).await;
    Ok(())
}

fn platform_api_key() -> eyre::Result<String> {
    std::env::var("CUFFLINK_PLATFORM_API_KEY")
        .map_err(|_| eyre::eyre!("CUFFLINK_PLATFORM_API_KEY env var required for preview commands"))
}

fn platform_auth_header(api_key: &str) -> String {
    format!("ApiKey {}", api_key)
}

#[derive(serde::Deserialize)]
struct TenantSummary {
    slug: String,
    keycloak_url: Option<String>,
    keycloak_realm: Option<String>,
}

pub async fn preview_create(
    name: &str,
    source_tenant: &str,
    env: Option<&str>,
) -> eyre::Result<()> {
    let (ws, root) = load_workspace()?;
    let env = ws.resolve_env(env);
    let config = load_config_from_workspace(&root, &ws, env)?;
    let api_key = platform_api_key()?;
    let preview_slug = format!("preview-{}", name);

    println!("Creating preview environment '{}'...", preview_slug);

    // Step 1: Fetch source tenant's keycloak config
    let http = reqwest::Client::new();
    let resp = http
        .get(format!("{}/api/tenants", config.api_url))
        .header("Authorization", platform_auth_header(&api_key))
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to list tenants ({}): {}", status, body);
    }

    let tenants: Vec<TenantSummary> = resp.json().await?;
    let source = tenants
        .iter()
        .find(|t| t.slug == source_tenant)
        .ok_or_else(|| eyre::eyre!("Source tenant '{}' not found", source_tenant))?;

    // Step 2: Create preview tenant with same keycloak config
    println!("  Creating tenant '{}'...", preview_slug);
    let create_body = serde_json::json!({
        "name": format!("Preview {}", name),
        "slug": preview_slug,
        "keycloak_url": source.keycloak_url,
        "keycloak_realm": source.keycloak_realm,
    });

    let resp = http
        .post(format!("{}/api/tenants", config.api_url))
        .header("Authorization", platform_auth_header(&api_key))
        .json(&create_body)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to create tenant ({}): {}", status, body);
    }
    println!("  Tenant created");

    // Step 3: Deploy all services to preview tenant (parallel)
    let deploy_services = ws.deploy_list(env);
    println!(
        "  Deploying {} services in parallel...",
        deploy_services.len()
    );
    let results = deploy_parallel(&root, &deploy_services, env, Some(&preview_slug)).await?;

    for (name, result) in &results {
        match result {
            Ok(_) => println!("    {} — deployed", name),
            Err(e) => println!("    {} — FAILED: {}", name, e),
        }
    }

    // Step 4: Clone data from source tenant
    println!("  Cloning data from '{}'...", source_tenant);

    // Find any service to get its ID for backup
    let resp = http
        .get(format!("{}/api/services", config.api_url))
        .header("Authorization", platform_auth_header(&api_key))
        .header("X-Tenant-Slug", source_tenant)
        .send()
        .await?;

    let services_resp: serde_json::Value = resp.json().await?;
    let source_services = services_resp["services"].as_array();

    if let Some(services) = source_services {
        for svc in services {
            let svc_id = match svc["id"].as_str() {
                Some(id) => id,
                None => continue,
            };
            let svc_name = svc["name"].as_str().unwrap_or("?");

            // Export from source
            let resp = http
                .post(format!(
                    "{}/api/services/{}/backup/export",
                    config.api_url, svc_id
                ))
                .header("Authorization", platform_auth_header(&api_key))
                .header("X-Tenant-Slug", source_tenant)
                .json(&serde_json::json!({}))
                .send()
                .await?;

            if !resp.status().is_success() {
                println!("    {} — export failed, skipping", svc_name);
                continue;
            }

            let export_resp: serde_json::Value = resp.json().await?;
            let export_job_id = match export_resp["job_id"].as_str() {
                Some(id) => id.to_string(),
                None => continue,
            };

            // Poll export
            let export_job = poll_platform_job(
                &http,
                &config.api_url,
                &api_key,
                source_tenant,
                svc_id,
                &export_job_id,
            )
            .await?;

            if export_job["status"].as_str() != Some("completed") {
                println!("    {} — export failed", svc_name);
                continue;
            }

            let s3_key = match export_job["s3_key"].as_str() {
                Some(k) => k,
                None => continue,
            };

            // Find preview service ID
            let resp = http
                .get(format!("{}/api/services", config.api_url))
                .header("Authorization", platform_auth_header(&api_key))
                .header("X-Tenant-Slug", &preview_slug)
                .send()
                .await?;

            let preview_services: serde_json::Value = resp.json().await?;
            let preview_svc_id = preview_services["services"].as_array().and_then(|arr| {
                arr.iter()
                    .find(|s| s["name"].as_str() == Some(svc_name))
                    .and_then(|s| s["id"].as_str())
            });

            let preview_svc_id = match preview_svc_id {
                Some(id) => id.to_string(),
                None => continue,
            };

            // Restore into preview
            let resp = http
                .post(format!(
                    "{}/api/services/{}/backup/restore",
                    config.api_url, preview_svc_id
                ))
                .header("Authorization", platform_auth_header(&api_key))
                .header("X-Tenant-Slug", &preview_slug)
                .json(&serde_json::json!({ "s3_key": s3_key }))
                .send()
                .await?;

            if !resp.status().is_success() {
                println!("    {} — restore failed", svc_name);
                continue;
            }

            let restore_resp: serde_json::Value = resp.json().await?;
            let restore_job_id = match restore_resp["job_id"].as_str() {
                Some(id) => id.to_string(),
                None => continue,
            };

            let restore_job = poll_platform_job(
                &http,
                &config.api_url,
                &api_key,
                &preview_slug,
                &preview_svc_id,
                &restore_job_id,
            )
            .await?;

            let rows = restore_job["processed_rows"].as_i64().unwrap_or(0);
            println!("    {} — {} rows cloned", svc_name, rows);
        }
    }

    println!();
    println!("Preview environment '{}' ready", preview_slug);

    Ok(())
}

pub async fn preview_destroy(name: &str, env: Option<&str>) -> eyre::Result<()> {
    let (ws, root) = load_workspace()?;
    let env = ws.resolve_env(env);
    let config = load_config_from_workspace(&root, &ws, env)?;
    let api_key = platform_api_key()?;
    let preview_slug = format!("preview-{}", name);

    println!("Destroying preview environment '{}'...", preview_slug);

    let http = reqwest::Client::new();
    let resp = http
        .delete(format!("{}/api/tenants/{}", config.api_url, preview_slug))
        .header("Authorization", platform_auth_header(&api_key))
        .send()
        .await?;

    if resp.status().is_success() {
        println!("Preview environment '{}' destroyed", preview_slug);
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!(
            "Failed to destroy preview environment ({}): {}",
            status,
            body
        );
    }

    Ok(())
}

async fn poll_platform_job(
    http: &reqwest::Client,
    api_url: &str,
    api_key: &str,
    tenant_slug: &str,
    service_id: &str,
    job_id: &str,
) -> eyre::Result<serde_json::Value> {
    loop {
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;

        let resp = http
            .get(format!(
                "{}/api/services/{}/backup/jobs/{}",
                api_url, service_id, job_id
            ))
            .header("Authorization", platform_auth_header(api_key))
            .header("X-Tenant-Slug", tenant_slug)
            .send()
            .await?;

        if !resp.status().is_success() {
            continue;
        }

        let job: serde_json::Value = resp.json().await?;
        match job["status"].as_str() {
            Some("completed" | "failed" | "cancelled") => return Ok(job),
            _ => continue,
        }
    }
}

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

    #[test]
    fn parses_list_tenants_bare_array() {
        let body = serde_json::json!([
            {
                "id": "00000000-0000-0000-0000-000000000000",
                "name": "Default",
                "slug": "default",
                "keycloak_url": "https://auth.staging.example",
                "keycloak_realm": "example",
                "deploy_role": "cufflink-deploy",
                "created_at": "2026-01-01T00:00:00Z"
            }
        ]);
        let tenants: Vec<TenantSummary> = serde_json::from_value(body).unwrap();
        assert_eq!(tenants.len(), 1);
        assert_eq!(tenants[0].slug, "default");
        assert_eq!(
            tenants[0].keycloak_url.as_deref(),
            Some("https://auth.staging.example"),
        );
        assert_eq!(tenants[0].keycloak_realm.as_deref(), Some("example"));
    }
}
