use crate::github::GitHubSource;
use crate::package_manifest::PackageManifest;
use comfy_table::{presets::NOTHING, Table, TableComponent};
use std::path::Path;

const PACKAGES_DIR: &str = "cufflink-packages";

pub async fn run(source: &str, env: Option<&str>) -> eyre::Result<()> {
    let (source_dir, _temp) = if source.starts_with("github:") {
        let gh = GitHubSource::parse(source)?;
        println!("Installing {}...", gh.display());
        let temp = tempfile::tempdir()?;
        crate::github::download(&gh, temp.path()).await?;
        (temp.path().to_path_buf(), Some(temp))
    } else {
        let path = std::path::PathBuf::from(shellexpand::tilde(source).as_ref());
        if !path.exists() {
            eyre::bail!("Path '{}' does not exist", path.display());
        }
        println!("Installing from {}...", path.display());
        (path, None)
    };

    let manifest = PackageManifest::load(&source_dir)?;
    manifest.validate_paths(&source_dir)?;

    let packages_dir = std::env::current_dir()?.join(PACKAGES_DIR);
    let dest = packages_dir.join(&manifest.package.name);

    if dest.exists() {
        print!(
            "Package '{}' already installed. Overwrite? [y/N] ",
            manifest.package.name
        );
        use std::io::Write;
        std::io::stdout().flush()?;
        let mut input = String::new();
        std::io::stdin().read_line(&mut input)?;
        if !input.trim().eq_ignore_ascii_case("y") {
            println!("Aborted.");
            return Ok(());
        }
        std::fs::remove_dir_all(&dest)?;
    }

    std::fs::create_dir_all(&packages_dir)?;

    let output = std::process::Command::new("cp")
        .args(["-r", &source_dir.to_string_lossy(), &dest.to_string_lossy()])
        .output()?;
    if !output.status.success() {
        eyre::bail!("Failed to copy package to {}", dest.display());
    }

    inject_environments(&manifest, &dest)?;
    update_lockfile(&packages_dir, &manifest, source)?;

    println!();
    manifest.print_summary();

    println!("Installed to {}/{}/", PACKAGES_DIR, manifest.package.name);
    println!();

    print!("Deploy all components? [y/N] ");
    use std::io::Write;
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;

    if input.trim().eq_ignore_ascii_case("y") {
        deploy_package(&manifest, &dest, env).await?;
    } else {
        println!("Skipped deploy. Deploy later from each component directory:");
        for s in &manifest.services {
            println!(
                "  cd {}/{}/{} && cufflink deploy",
                PACKAGES_DIR, manifest.package.name, s.path
            );
        }
        for f in &manifest.frontends {
            println!(
                "  cd {}/{}/{} && cufflink deploy",
                PACKAGES_DIR, manifest.package.name, f.path
            );
        }
    }

    Ok(())
}

/// Read environment config from the nearest ancestor Cufflink.toml and
/// append it to each component's Cufflink.toml so deploy can find
/// the API URL, tenant, and credentials.
fn inject_environments(manifest: &PackageManifest, package_dir: &Path) -> eyre::Result<()> {
    let parent_config = crate::project_config::ProjectConfig::find_and_load()?;
    let parent = match parent_config {
        Some(p) if !p.environments.is_empty() => p,
        _ => return Ok(()),
    };

    let parent_toml_path = parent.project_dir.join("Cufflink.toml");
    let parent_content = std::fs::read_to_string(&parent_toml_path)?;

    // Extract everything after [service] section — the environment blocks
    let env_sections = extract_environment_sections(&parent_content);
    if env_sections.is_empty() {
        return Ok(());
    }

    let component_paths: Vec<&str> = manifest
        .services
        .iter()
        .map(|s| s.path.as_str())
        .chain(manifest.frontends.iter().map(|f| f.path.as_str()))
        .collect();

    for component_path in component_paths {
        let toml_path = package_dir.join(component_path).join("Cufflink.toml");
        if !toml_path.exists() {
            continue;
        }

        let content = std::fs::read_to_string(&toml_path)?;
        if content.contains("[environments.") {
            continue;
        }

        let mut new_content = content.trim_end().to_string();
        new_content.push_str("\n\n");
        new_content.push_str(&env_sections);
        new_content.push('\n');
        std::fs::write(&toml_path, new_content)?;
    }

    // Also inject default_env into [service] if not set
    for component_path in manifest
        .services
        .iter()
        .map(|s| s.path.as_str())
        .chain(manifest.frontends.iter().map(|f| f.path.as_str()))
    {
        let toml_path = package_dir.join(component_path).join("Cufflink.toml");
        if !toml_path.exists() {
            continue;
        }
        let content = std::fs::read_to_string(&toml_path)?;
        if !content.contains("default_env") {
            if let Some(default_env) = &parent.service.default_env {
                let updated = content.replacen(
                    "[service]\n",
                    &format!("[service]\ndefault_env = \"{}\"\n", default_env),
                    1,
                );
                std::fs::write(&toml_path, updated)?;
            }
        }
    }

    Ok(())
}

fn extract_environment_sections(toml_content: &str) -> String {
    let mut result = String::new();
    let mut in_env_section = false;

    for line in toml_content.lines() {
        if line.starts_with("[environments.") || line.starts_with("[environments]") {
            in_env_section = true;
        } else if line.starts_with('[') && !line.starts_with("[environments") {
            in_env_section = false;
        }

        if in_env_section {
            result.push_str(line);
            result.push('\n');
        }
    }

    result
}

fn update_lockfile(
    packages_dir: &Path,
    manifest: &PackageManifest,
    source: &str,
) -> eyre::Result<()> {
    let lockfile = packages_dir.join("cufflink-packages.toml");
    let mut content = std::fs::read_to_string(&lockfile).unwrap_or_default();

    let version = manifest.package.version.as_deref().unwrap_or("0.0.0");
    let entry = format!(
        "\n[{}]\nversion = \"{}\"\nsource = \"{}\"\ninstalled_at = \"{}\"\n",
        manifest.package.name,
        version,
        source,
        chrono::Utc::now().to_rfc3339(),
    );

    if let Some(start) = content.find(&format!("[{}]", manifest.package.name)) {
        let end = content[start + 1..]
            .find("\n[")
            .map(|i| start + 1 + i)
            .unwrap_or(content.len());
        content.replace_range(start..end, "");
    }

    content.push_str(&entry);
    std::fs::write(&lockfile, content.trim_start())?;
    Ok(())
}

async fn deploy_package(
    manifest: &PackageManifest,
    package_dir: &Path,
    env: Option<&str>,
) -> eyre::Result<()> {
    if !manifest.config.required.is_empty() {
        println!("Note: Set these config keys after deployment:");
        for key in &manifest.config.required {
            println!("  cufflink config set {} <value>", key);
        }
        println!();
    }

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

    for service in &manifest.services {
        let service_dir = package_dir.join(&service.path);
        println!("Deploying service '{}'...", service.name);
        std::env::set_current_dir(&service_dir)?;

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

    for frontend in &manifest.frontends {
        let frontend_dir = package_dir.join(&frontend.path);
        println!("Deploying frontend '{}'...", frontend.name);
        std::env::set_current_dir(&frontend_dir)?;

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

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

    println!();

    let mut table = Table::new();
    table.load_preset(NOTHING);
    table.set_style(TableComponent::HeaderLines, '-');
    table.set_style(TableComponent::MiddleHeaderIntersections, ' ');
    table.set_header(vec!["COMPONENT", "TYPE", "STATUS"]);

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

    println!("{table}");

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

    Ok(())
}
