use anyhow::Result;
use colored::*;
use ignore::WalkBuilder;
use ignore::overrides::OverrideBuilder;
use std::fs;
use std::path::Path;
use tracing;

use crate::commands::update::run_update;
use crate::npm_package::find_npm_package;
use crate::registry::*;
use crate::utils::normalized_path;

/// Publish logic
pub fn publish(registry: &mut Registry, push: bool) -> Result<()> {
    let pkg = find_npm_package(&std::env::current_dir()?)?;

    println!(
        "🚀 Publishing {}@{}...",
        pkg.name.cyan(),
        pkg.version.magenta()
    );

    // Determine the path in the registry (~/.kley/packages/name)
    let pkg_in_registry = registry.get_pkg_dir(&pkg.name);

    if pkg_in_registry.exists() {
        fs::remove_dir_all(&pkg_in_registry)?;
    }
    fs::create_dir_all(&pkg_in_registry)?;

    tracing::debug!("Created dir {:?}", &pkg_in_registry);

    // Apply npm built-in rules via OverrideBuilder
    let mut override_builder = OverrideBuilder::new(Path::new("."));
    // Exclude:
    override_builder.add("!.git/")?;
    override_builder.add("!node_modules/")?;
    override_builder.add("!*.swp")?;
    override_builder.add("!._*")?;
    override_builder.add("!.DS_Store")?;
    override_builder.add("!npm-debug.log*")?;
    override_builder.add("!package-lock.json")?;
    override_builder.add("!yarn.lock")?;
    override_builder.add("!pnpm-lock.yaml")?;
    override_builder.add("!.npmrc")?;

    // Whitelist
    // NOTE: If an override contains one or more positive patterns,
    // then it will ignore any file path that does not match at least one of those positive patterns
    if let Some(files) = &pkg.files {
        for file in files {
            // add files
            override_builder.add(file)?;
            // and folders
            override_builder.add(&format!("{}/**", file))?;
        }
    }

    let walk_with_ignores = WalkBuilder::new(".")
        .hidden(false)
        .git_ignore(!Path::new(".npmignore").exists()) // Correctly use .gitignore as a fallback
        .add_custom_ignore_filename(".npmignore")
        .add_custom_ignore_filename(".kleyignore")
        .overrides(override_builder.build()?)
        .build();

    for entry in walk_with_ignores {
        let entry = entry?;
        let path = entry.path();

        if path == Path::new(".") {
            continue;
        }

        // Skip only dirs without files
        if path.is_dir() {
            continue;
        }

        tracing::debug!(path = %path.to_string_lossy(), "Packing entry");

        let relative_path = path.strip_prefix(".")?;
        let target_path = pkg_in_registry.join(relative_path);

        if path.is_dir() {
            fs::create_dir_all(&target_path)?;
        } else {
            if let Some(parent) = target_path.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::copy(path, &target_path)?;
        }
    }

    let mandatory_files = [
        "package.json",
        "README.md",
        "README",
        "readme.md",
        "LICENSE",
        "LICENSE.md",
        "CHANGELOG.md",
    ];

    for mf in mandatory_files {
        let mf_path = Path::new(mf);
        if mf_path.is_file() {
            let target = pkg_in_registry.join(mf);
            fs::copy(mf_path, target).ok();
        }
    }

    registry.update_package_version(&pkg.name, &pkg.version)?;

    println!("📦 Package '{}' saved to registry", pkg.name.cyan().bold());

    if push {
        let instalations = registry.get_installations(&pkg.name).to_vec();
        let instalation_len = instalations.len();

        if instalation_len > 0 {
            let plural_text = if instalation_len == 1 {
                "project"
            } else {
                "projects"
            };

            println!(
                "Pushing {} to {} {plural_text}:",
                pkg.name.cyan(),
                instalation_len
            );

            for project_dir in instalations {
                run_update(registry, &pkg.name, &project_dir)?;

                println!(
                    "{}",
                    format!(
                        "✔️ Updated {} to the latest version of {}",
                        normalized_path(&project_dir, dirs::home_dir().as_ref()).white(),
                        &pkg.name.cyan()
                    )
                    .green()
                );
            }
        }
    } else {
        let add_cmd = format!("     kley add {}", pkg.name).cyan();
        let link_cmd = format!("     kley link {}", pkg.name).cyan();

        let note_msg = format!("To use it, run:\n{}\nor\n{}", add_cmd, link_cmd,)
            .italic()
            .dimmed();

        println!("{note_msg}");
    }

    println!(
        "{}",
        format!("✅ Done: {} published", pkg.name.cyan()).green()
    );

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::Result;
    use std::fs;
    use std::path::Path;
    use tempfile::tempdir;

    /// Helper to create a dummy project structure inside a temporary directory.
    fn setup_test_project(dir: &Path) -> Result<()> {
        fs::create_dir_all(dir.join(".git"))?; // Trick the `ignore` crate into thinking this is a git repo
        fs::write(
            dir.join("package.json"),
            r#"{"name": "test-pkg", "version": "1.0.0"}"#,
        )?;
        fs::write(dir.join("index.js"), "console.log('hello');")?;
        fs::create_dir_all(dir.join("dist"))?;
        fs::write(dir.join("dist/bundle.js"), "/* bundle */")?;
        fs::write(dir.join("secret.log"), "sensitive data")?;
        // This file should also be ignored by default git rules
        fs::create_dir_all(dir.join("node_modules/some_dep"))?;
        fs::write(dir.join("node_modules/some_dep/index.js"), "...")?;
        Ok(())
    }

    #[test]
    fn test_publish_filtering_logic() -> Result<()> {
        let original_dir = std::env::current_dir()?;
        let tmp_home_dir = tempdir()?;
        let home_dir = tmp_home_dir.path();
        let store_path = home_dir.join(".kley/packages/test-pkg");

        let mut registry = Registry::new(home_dir.to_path_buf())?;

        // --- SCENARIO 1: .npmignore exists ---
        {
            let proj_dir = tempdir()?;
            let proj_path = proj_dir.path();
            setup_test_project(proj_path)?;

            fs::write(
                proj_path.join(".gitignore"),
                "dist\nsecret.log\nnode_modules",
            )?;
            fs::write(proj_path.join(".npmignore"), "secret.log")?;

            std::env::set_current_dir(proj_path)?;
            publish(&mut registry, false)?;

            // Assert: build artifact IS included, secret IS NOT, node_modules IS NOT
            assert!(
                store_path.join("dist/bundle.js").exists(),
                "Scenario 1: dist/bundle.js should exist"
            );
            assert!(
                !store_path.join("secret.log").exists(),
                "Scenario 1: secret.log should NOT exist"
            );
            assert!(
                !store_path.join("node_modules").exists(),
                "Scenario 1: node_modules should NOT exist"
            );

            // Cleanup for next scenario
            fs::remove_dir_all(&store_path)?;
        }

        // --- SCENARIO 2: Only .gitignore exists ---
        {
            let proj_dir = tempdir()?;
            let proj_path = proj_dir.path();
            setup_test_project(proj_path)?;
            fs::write(
                proj_path.join(".gitignore"),
                "dist\nsecret.log\nnode_modules",
            )?;

            std::env::set_current_dir(proj_path)?;
            publish(&mut registry, false)?;

            // Assert: build artifact IS NOT included, secret IS NOT, node_modules IS NOT
            assert!(
                !store_path.join("dist/bundle.js").exists(),
                "Scenario 2: dist/bundle.js should NOT exist"
            );
            assert!(
                !store_path.join("secret.log").exists(),
                "Scenario 2: secret.log should NOT exist"
            );
            assert!(
                !store_path.join("node_modules").exists(),
                "Scenario 2: node_modules should NOT exist"
            );
        }

        // --- Final Cleanup ---
        std::env::set_current_dir(original_dir)?;
        if store_path.exists() {
            fs::remove_dir_all(&store_path)?;
        }

        Ok(())
    }
}
