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

use crate::emoji;
use crate::registry::Registry;

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 copy_from_registry(
    registry: &Registry,
    package_name: &str,
    project_dir: &Path,
) -> Result<()> {
    tracing::debug!(
        "copy_from_registry:\n package_name: {package_name}\n project_dir: {project_dir:?}"
    );

    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
        );
    }

    let project_kley_dir = project_dir
        .join(PROJECT_REGISTRY_DIR_NAME)
        .join(package_name);

    if project_kley_dir.exists() {
        tracing::debug!("copy_from_registry: removing existing dir {:?}", project_kley_dir);
        fs::remove_dir_all(&project_kley_dir).map_err(|e| {
            tracing::debug!("copy_from_registry: remove_dir_all failed: {e:?}");
            e
        })?;
    }
    tracing::debug!("copy_from_registry: creating dir {:?}", project_kley_dir);
    fs::create_dir_all(&project_kley_dir)?;

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

    tracing::info!(
        "Package {} was copied from registry to {} dir",
        package_name,
        project_dir.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 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}"
        );
    }
}
