use anyhow::Result;
use std::path::{Path, PathBuf};

use crate::pom::Pom;

/// Returns the path to the local pman repository (`~/.pman/repository`).
#[must_use]
pub fn local_repo_path() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".pman")
        .join("repository")
}

/// Compute the path for an artifact in the local repository.
/// Layout: `{repo}/{groupId/with/slashes}/{artifactId}/{version}/{artifactId}-{version}.{extension}`
#[must_use]
pub fn artifact_path(
    repo: &Path,
    group_id: &str,
    artifact_id: &str,
    version: &str,
    extension: &str,
) -> PathBuf {
    let group_path = group_id.replace('.', "/");
    repo.join(group_path)
        .join(artifact_id)
        .join(version)
        .join(format!("{artifact_id}-{version}.{extension}"))
}

/// Validate that artifact path components are safe and cannot escape the repository root
/// via path traversal sequences.
///
/// # Errors
///
/// Returns an error if any component is empty, contains `..`, path separators, or starts
/// with a `.` (which would create hidden directories).
pub(crate) fn validate_artifact_components(
    group_id: &str,
    artifact_id: &str,
    version: &str,
) -> Result<()> {
    for (component, label) in [
        (group_id, "groupId"),
        (artifact_id, "artifactId"),
        (version, "version"),
    ] {
        if component.is_empty() {
            anyhow::bail!("artifact path component {label} must not be empty");
        }
        if component.contains("..") || component.contains('/') || component.contains('\\') {
            anyhow::bail!(
                "unsafe artifact path component {label}: {component:?} contains illegal characters"
            );
        }
    }
    Ok(())
}

/// Install an artifact (JAR + POM) into the local repository.
///
/// # Errors
///
/// Returns an error if directories cannot be created or files cannot be copied/written.
pub fn install_artifact(repo: &Path, pom: &Pom, jar_path: &Path) -> Result<()> {
    let group_id = pom.effective_group_id();
    let artifact_id = &pom.artifact_id;
    let version = pom.effective_version();

    validate_artifact_components(group_id, artifact_id, version)?;

    let dest_jar = artifact_path(repo, group_id, artifact_id, version, "jar");
    let dest_pom = artifact_path(repo, group_id, artifact_id, version, "pom");

    // Belt-and-suspenders: ensure constructed paths stay within the repo root.
    if !dest_jar.starts_with(repo) || !dest_pom.starts_with(repo) {
        anyhow::bail!("artifact path would escape the repository directory");
    }

    if let Some(parent) = dest_jar.parent() {
        std::fs::create_dir_all(parent)?;
    }

    std::fs::copy(jar_path, &dest_jar)?;
    println!(
        "[INFO] Installed {} to {}",
        jar_path.display(),
        dest_jar.display()
    );

    // Serialize POM back to XML and write it
    let pom_xml = quick_xml::se::to_string(pom)?;
    std::fs::write(
        &dest_pom,
        format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n{pom_xml}"),
    )?;
    println!("[INFO] Installed POM to {}", dest_pom.display());

    Ok(())
}

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

    #[test]
    fn test_artifact_path_simple() {
        let repo = Path::new("/home/user/.pman/repository");
        let path = artifact_path(repo, "com.example", "my-app", "1.0.0", "jar");
        assert_eq!(
            path,
            PathBuf::from("/home/user/.pman/repository/com/example/my-app/1.0.0/my-app-1.0.0.jar")
        );
    }

    #[test]
    fn test_artifact_path_pom() {
        let repo = Path::new("/repo");
        let path = artifact_path(repo, "org.apache.maven", "maven-core", "3.9.0", "pom");
        assert_eq!(
            path,
            PathBuf::from("/repo/org/apache/maven/maven-core/3.9.0/maven-core-3.9.0.pom")
        );
    }

    #[test]
    fn test_local_repo_path_suffix() {
        let path = local_repo_path();
        // The path must end with .pman/repository regardless of where the home dir is.
        let expected_suffix = Path::new(".pman").join("repository");
        assert!(
            path.ends_with(&expected_suffix),
            "expected path to end with .pman/repository, got: {}",
            path.display()
        );
    }

    #[test]
    fn test_validate_artifact_components_ok() {
        assert!(validate_artifact_components("com.example", "my-app", "1.0.0").is_ok());
        assert!(
            validate_artifact_components("org.apache.maven", "maven-core", "3.9.0-SNAPSHOT")
                .is_ok()
        );
    }

    #[test]
    fn test_validate_artifact_components_rejects_traversal() {
        assert!(validate_artifact_components("../etc", "app", "1.0").is_err());
        assert!(validate_artifact_components("com.example", "../secret", "1.0").is_err());
        assert!(validate_artifact_components("com.example", "app", "../../etc/passwd").is_err());
    }

    #[test]
    fn test_validate_artifact_components_rejects_separators() {
        assert!(validate_artifact_components("com/example", "app", "1.0").is_err());
        assert!(validate_artifact_components("com.example", "app\\module", "1.0").is_err());
    }

    #[test]
    fn test_validate_artifact_components_rejects_empty() {
        assert!(validate_artifact_components("", "app", "1.0").is_err());
        assert!(validate_artifact_components("com.example", "", "1.0").is_err());
        assert!(validate_artifact_components("com.example", "app", "").is_err());
    }
}
