use anyhow::Result;
use chrono::{DateTime, Utc};
use colored::*;
use serde_json::Value;
use std::{
    fs,
    io::{self, Write},
    path::{Path, PathBuf},
};

use crate::registry::Registry;
use crate::{emoji, package::PackageJson};

pub static PROJECT_REGISTRY_DIR_NAME: &str = ".kley";

pub struct WorkDirs {
    pub project_dir: PathBuf,
    pub project_kley_dir: PathBuf,
}

pub fn work_dirs(package_name: &str) -> Result<WorkDirs> {
    let project_dir = std::env::current_dir()?;
    let project_kley_dir = project_dir
        .join(PROJECT_REGISTRY_DIR_NAME)
        .join(package_name);

    Ok(WorkDirs {
        project_dir,
        project_kley_dir,
    })
}

pub fn strip_dev_dependencies(package_dir: &Path) -> Result<()> {
    if !package_dir.exists() {
        println!(
            "{} Warning: directory '{}' does not exist",
            emoji::WARNING,
            package_dir.to_string_lossy()
        );

        return Ok(());
    }

    let package_json_content = PackageJson::get_raw(package_dir)?;

    let indent = detect_indent(&package_json_content);

    let mut value: Value = serde_json::from_str(&package_json_content)?;

    if let Some(obj) = value.as_object_mut()
        && obj.remove("devDependencies").is_some()
    {
        PackageJson::save_raw(value, package_dir, &indent)?;
    }

    Ok(())
}

pub fn copy_from_registry(
    registry: &Registry,
    package_name: &str,
    destination: &Path,
) -> Result<()> {
    tracing::debug!(
        "copy_from_registry:\n package_name: {package_name}\n destination: {destination:?}"
    );

    let registry_dir = registry.dir_path.join("packages").join(package_name);
    tracing::debug!("registry_dir: {registry_dir:?}");

    if !registry_dir.exists() {
        anyhow::bail!(
            "Package '{}' not found inside registry. Run 'kley publish' in the package folder first.",
            package_name
        );
    }

    if destination.exists() {
        tracing::debug!(
            "copy_from_registry: removing existing dir '{:?}' content",
            destination
        );

        for entry in fs::read_dir(destination)?.filter_map(|e| e.ok())
        // ignore errors, it's not so matter to remove all files, below we will overwrite obsolete files anyway
        {
            let path = entry.path();
            let file_type = entry.file_type()?;

            if let Some(name) = path.file_name()
                && name == "node_modules"
                && file_type.is_dir()
            {
                continue;
            }

            if file_type.is_file() {
                fs::remove_file(&path)?;
            }

            if file_type.is_dir() {
                fs::remove_dir_all(&path)?;
            }
        }
    } else {
        tracing::debug!("copy_from_registry: creating dir {:?}", destination);
        fs::create_dir_all(destination)?;
    }

    // Copy from store to local project
    let mut options = fs_extra::dir::CopyOptions::new();
    options.content_only = true;
    options.overwrite = true;
    fs_extra::dir::copy(registry_dir, destination, &options)?;

    tracing::info!(
        "Package {} was copied from registry to {} dir",
        package_name,
        destination.display()
    );

    Ok(())
}

/// Detects the indentation of a JSON string.
pub fn detect_indent(json_str: &str) -> String {
    for line in json_str.lines() {
        if line.starts_with("  ") {
            let indent_len = line.find(|c: char| !c.is_whitespace()).unwrap_or(0);
            if indent_len > 0 {
                return line[..indent_len].to_string();
            }
        }
    }
    "  ".to_string() // Default to 2 spaces
}

/// Get the current time in UTC
pub fn current_formatted_time() -> String {
    let now_utc: DateTime<Utc> = Utc::now();

    now_utc.format("%Y-%m-%dT%H:%M:%SZ").to_string()
}

/// Confirm to proceed or abort
pub fn confirm(prompt: ColoredString) -> bool {
    loop {
        print!("{} (y/n): ", prompt);
        io::stdout().flush().unwrap(); // Ensure the prompt is printed before reading

        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read line");

        match input.trim().to_lowercase().as_str() {
            "y" | "yes" => return true,
            "n" | "no" => return false,
            _ => println!("Please enter 'y' for yes or 'n' for no."),
        }
    }
}

pub fn normalized_path(path: &Path, home: Option<&PathBuf>) -> String {
    // On Unix, canonicalize resolves symlinks (e.g. /var/folders → /private/var/folders)
    // which is needed for strip_prefix to match paths correctly.
    // On Windows, canonicalize adds the \\?\ UNC prefix which breaks strip_prefix
    // and produces ugly output, so we skip it.
    #[cfg(not(windows))]
    let path = fs::canonicalize(path).unwrap_or(path.to_path_buf());

    if let Some(home_dir) = home
        && let Ok(stripped_path) = path.strip_prefix(home_dir)
    {
        return format!("~/{}", stripped_path.display());
    }

    path.to_string_lossy().into_owned()
}

pub fn get_kley_home_dir() -> Result<PathBuf> {
    std::env::var("KLEY_HOME").map(PathBuf::from).or_else(|_| {
        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Failed to find home directory"))
    })
}

pub fn package_name_version_parse(package_name_version: &str) -> (&str, Option<&str>) {
    if let Some((name, version)) = package_name_version.rsplit_once('@')
        && !name.is_empty()
    {
        return (name, Some(version));
    }

    (package_name_version, None)
}

pub fn validate_version_in_registry(
    registry: &Registry,
    package_name: &str,
    package_version: Option<&str>,
) {
    let registry_pkg_version = registry.get_pkg_version(package_name);

    if registry_pkg_version.is_none()
        || !registry.has_version_in_registry(package_name, package_version)
    {
        let help = if registry_pkg_version.is_some() {
            format!(
                "Try to add the latest version from the registry. There is no {} version",
                package_version.unwrap_or("expected").magenta()
            )
        } else {
            format!(
                "Run `kley publish` in the {} package folder first",
                package_name.cyan()
            )
        };

        let version_msg = if let Some(ver) = package_version {
            format!("@{}", ver.magenta())
        } else {
            String::new()
        };

        eprintln!(
            "{}\n{}",
            format!(
                "{} Error: {}{} not found in the registry",
                emoji::ERROR,
                package_name.cyan(),
                version_msg,
            )
            .red(),
            help.italic().bright_black()
        );

        std::process::exit(1);
    }
}

#[cfg(test)]
mod copy_from_registry_tests {
    use super::*;
    use crate::registry::Registry;
    use std::fs;
    use tempfile::tempdir;

    /// Helper: create a package in the store (registry_dir/packages/pkg_name/)
    fn create_store_package(
        registry_dir: &Path,
        pkg_name: &str,
        pkg_version: &str,
        extra_files: &[(&str, &str)],
    ) {
        let pkg_dir = registry_dir.join("packages").join(pkg_name);
        fs::create_dir_all(&pkg_dir).unwrap();
        fs::write(
            pkg_dir.join("package.json"),
            format!(
                r#"{{"name": "{}", "version": "{}"}}"#,
                pkg_name, pkg_version
            ),
        )
        .unwrap();
        for (path, content) in extra_files {
            let full_path = pkg_dir.join(path);
            if let Some(parent) = full_path.parent() {
                fs::create_dir_all(parent).unwrap();
            }
            fs::write(full_path, content).unwrap();
        }
    }

    /// Helper: simulate npm having installed dependencies into .kley/<pkg>/node_modules/
    fn simulate_npm_deps(project_dir: &Path, pkg_name: &str) {
        let nm_dir = project_dir
            .join(PROJECT_REGISTRY_DIR_NAME)
            .join(pkg_name)
            .join("node_modules")
            .join("some-dep");
        fs::create_dir_all(&nm_dir).unwrap();
        fs::write(nm_dir.join("index.js"), "// some-dep code").unwrap();
    }

    #[test]
    fn test_copy_from_registry_preserves_node_modules_on_update() {
        // Setup: registry with v1, project with v1 already installed + node_modules
        let home_dir = tempdir().unwrap();
        let mut registry = Registry::with_home_dir(home_dir.path()).unwrap();
        registry.update_package_version("my-lib", "1.0.0").unwrap();

        let project_dir = home_dir.path().join("my-project");
        fs::create_dir_all(&project_dir).unwrap();

        // Create v1 in store
        create_store_package(
            &registry.dir_path,
            "my-lib",
            "1.0.0",
            &[("index.js", "module.exports = 'v1';")],
        );

        let project_kley_dir = project_dir.join(PROJECT_REGISTRY_DIR_NAME).join("my-lib");

        // First copy (simulates initial install)
        copy_from_registry(&registry, "my-lib", &project_kley_dir).unwrap();

        // Simulate npm installing deps into .kley/my-lib/node_modules/
        simulate_npm_deps(&project_dir, "my-lib");

        // Verify node_modules exists before update
        let nm_path = project_kley_dir.join("node_modules");
        assert!(nm_path.exists(), "node_modules should exist before update");

        // Update store to v2
        fs::remove_dir_all(registry.dir_path.join("packages").join("my-lib")).unwrap();
        create_store_package(
            &registry.dir_path,
            "my-lib",
            "2.0.0",
            &[("index.js", "module.exports = 'v2';")],
        );
        registry.update_package_version("my-lib", "2.0.0").unwrap();

        // Second copy (simulates publish --push / update)
        copy_from_registry(&registry, "my-lib", &project_kley_dir).unwrap();

        assert!(
            nm_path.exists(),
            "node_modules should be preserved after update"
        );

        // Source files should be updated
        let index_content = fs::read_to_string(
            project_dir
                .join(PROJECT_REGISTRY_DIR_NAME)
                .join("my-lib")
                .join("index.js"),
        )
        .unwrap();
        assert_eq!(index_content, "module.exports = 'v2';");

        // Deps inside node_modules should be intact
        let dep_content = fs::read_to_string(nm_path.join("some-dep/index.js")).unwrap();
        assert_eq!(dep_content, "// some-dep code");
    }

    #[test]
    fn test_copy_from_registry_stale_files_removed_on_update() {
        // Setup: v1 has "old-file.js", v2 does not — old-file.js should be removed
        let home_dir = tempdir().unwrap();
        let mut registry = Registry::with_home_dir(home_dir.path()).unwrap();
        registry.update_package_version("my-lib", "1.0.0").unwrap();

        let project_dir = home_dir.path().join("my-project");
        fs::create_dir_all(&project_dir).unwrap();

        // v1 in store: has old-file.js
        create_store_package(
            &registry.dir_path,
            "my-lib",
            "1.0.0",
            &[
                ("index.js", "module.exports = 'v1';"),
                ("old-file.js", "// should be removed in v2"),
            ],
        );

        let project_kley_dir = project_dir.join(PROJECT_REGISTRY_DIR_NAME).join("my-lib");

        copy_from_registry(&registry, "my-lib", &project_kley_dir).unwrap();
        assert!(
            project_kley_dir.join("old-file.js").exists(),
            "old-file.js should exist in v1"
        );

        // Simulate npm deps
        simulate_npm_deps(&project_dir, "my-lib");

        // v2 in store: no old-file.js, but has new-file.js
        fs::remove_dir_all(registry.dir_path.join("packages").join("my-lib")).unwrap();
        create_store_package(
            &registry.dir_path,
            "my-lib",
            "2.0.0",
            &[
                ("index.js", "module.exports = 'v2';"),
                ("new-file.js", "// added in v2"),
            ],
        );
        registry.update_package_version("my-lib", "2.0.0").unwrap();

        copy_from_registry(&registry, "my-lib", &project_kley_dir).unwrap();

        // old-file.js should be gone
        assert!(
            !project_kley_dir.join("old-file.js").exists(),
            "old-file.js should be removed in v2"
        );

        // new-file.js should exist
        assert!(
            project_dir
                .join(PROJECT_REGISTRY_DIR_NAME)
                .join("my-lib")
                .join("new-file.js")
                .exists(),
            "new-file.js should exist in v2"
        );

        // Source files updated
        let index_content = fs::read_to_string(
            project_dir
                .join(PROJECT_REGISTRY_DIR_NAME)
                .join("my-lib")
                .join("index.js"),
        )
        .unwrap();
        assert_eq!(index_content, "module.exports = 'v2';");

        // node_modules preserved
        let nm_path = project_dir
            .join(PROJECT_REGISTRY_DIR_NAME)
            .join("my-lib")
            .join("node_modules");
        assert!(
            nm_path.exists(),
            "node_modules should be preserved after update"
        );
    }

    #[test]
    fn test_copy_from_registry_first_install_creates_dir() {
        // Fresh install — no .kley/<pkg>/ exists yet
        let home_dir = tempdir().unwrap();
        let mut registry = Registry::with_home_dir(home_dir.path()).unwrap();
        registry.update_package_version("my-lib", "1.0.0").unwrap();

        let project_dir = home_dir.path().join("my-project");
        fs::create_dir_all(&project_dir).unwrap();

        create_store_package(
            &registry.dir_path,
            "my-lib",
            "1.0.0",
            &[("index.js", "module.exports = 'v1';")],
        );

        let project_kley_dir = project_dir.join(PROJECT_REGISTRY_DIR_NAME).join("my-lib");

        copy_from_registry(&registry, "my-lib", &project_kley_dir).unwrap();

        let index_content = fs::read_to_string(project_kley_dir.join("index.js")).unwrap();
        assert_eq!(index_content, "module.exports = 'v1';");
    }

    #[test]
    fn test_copy_from_registry_no_node_modules_to_preserve() {
        // Update without node_modules — should work fine
        let home_dir = tempdir().unwrap();
        let mut registry = Registry::with_home_dir(home_dir.path()).unwrap();
        registry.update_package_version("my-lib", "1.0.0").unwrap();

        let project_dir = home_dir.path().join("my-project");
        fs::create_dir_all(&project_dir).unwrap();

        create_store_package(&registry.dir_path, "my-lib", "1.0.0", &[("index.js", "v1")]);

        let project_kley_dir = project_dir.join(PROJECT_REGISTRY_DIR_NAME).join("my-lib");

        copy_from_registry(&registry, "my-lib", &project_kley_dir).unwrap();

        // Update without simulating npm deps (e.g. user never ran npm install)
        fs::remove_dir_all(registry.dir_path.join("packages").join("my-lib")).unwrap();
        create_store_package(&registry.dir_path, "my-lib", "2.0.0", &[("index.js", "v2")]);
        registry.update_package_version("my-lib", "2.0.0").unwrap();

        copy_from_registry(&registry, "my-lib", &project_kley_dir).unwrap();

        let index_content = fs::read_to_string(project_kley_dir.join("index.js")).unwrap();
        assert_eq!(index_content, "v2");
    }
}

#[cfg(test)]
mod tests {
    use super::{normalized_path, package_name_version_parse};
    use std::path::PathBuf;

    #[test]
    fn test_package_name_version_parsing() {
        assert_eq!(
            package_name_version_parse("my-pkg@1.2.3"),
            ("my-pkg", Some("1.2.3"))
        );
        assert_eq!(package_name_version_parse("my-pkg"), ("my-pkg", None));
        assert_eq!(
            package_name_version_parse("@scope/pkg@1.2.3"),
            ("@scope/pkg", Some("1.2.3"))
        );
        assert_eq!(
            package_name_version_parse("@scope/pkg"),
            ("@scope/pkg", None)
        );
        assert_eq!(package_name_version_parse("a@b@c"), ("a@b", Some("c")));
        assert_eq!(package_name_version_parse(""), ("", None));
        assert_eq!(package_name_version_parse("@"), ("@", None));
    }

    #[test]
    fn test_normalized_path_inside_home() {
        let home = PathBuf::from("/home/user");
        let path = home.join("projects").join("app");
        let result = normalized_path(&path, Some(&home));
        assert_eq!(PathBuf::from(result), PathBuf::from("~/projects/app"));
    }

    #[test]
    fn test_normalized_path_outside_home() {
        let home = PathBuf::from("/home/user");
        let path = PathBuf::from("/tmp/some-project");
        let result = normalized_path(&path, Some(&home));
        assert_eq!(PathBuf::from(result), path);
    }

    #[test]
    fn test_normalized_path_no_home() {
        let path = PathBuf::from("/tmp/some-project");
        let result = normalized_path(&path, None);
        assert_eq!(PathBuf::from(result), path);
    }

    #[test]
    fn test_normalized_path_home_itself() {
        let home = PathBuf::from("/home/user");
        assert_eq!(normalized_path(&home, Some(&home)), "~/");
    }

    #[test]
    fn test_normalized_path_never_contains_unc_prefix() {
        let home = PathBuf::from(r"C:\Users\user");
        let path = PathBuf::from(r"C:\Users\user\projects\app");
        let result = normalized_path(&path, Some(&home));
        assert!(
            !result.contains(r"\\?\"),
            "UNC prefix should never appear in output: {result}"
        );
    }
}
