use anyhow::{Context, Result};
use sha1::{Digest, Sha1};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::pom::{Dependency, Pom};
use crate::repository::{artifact_path, validate_artifact_components};

const MAVEN_CENTRAL: &str = "https://repo1.maven.org/maven2";

/// A dependency that has been resolved to a local filesystem path.
#[derive(Debug, Clone)]
pub struct ResolvedDependency {
    pub path: PathBuf,
    pub group_id: String,
    pub artifact_id: String,
    pub version: String,
}

/// Resolve all compile-scope dependencies for the given POM.
/// Downloads from Maven Central if not already present in the local repo.
///
/// `extra_props` contains CLI-supplied `-D` overrides (e.g. from `pman -Dmy.ver=2.0`);
/// these are applied on top of POM-level property resolution.
///
/// # Errors
///
/// Returns an error if a dependency cannot be downloaded or its checksum does not match.
pub fn resolve_dependencies(
    pom: &Pom,
    local_repo: &Path,
    extra_props: &HashMap<String, String>,
) -> Result<Vec<ResolvedDependency>> {
    let mut resolved = Vec::new();

    for dep in &pom.dependencies.items {
        // Skip optional and non-compile-scope deps for classpath purposes
        if dep.optional {
            continue;
        }
        if dep.scope == "provided" || dep.scope == "system" || dep.scope == "import" {
            continue;
        }

        // Resolve version: POM-level properties first, then CLI overrides on top.
        let pom_resolved = pom.resolve_property(&dep.version);
        let version = extra_props.iter().fold(pom_resolved, |acc, (k, v)| {
            acc.replace(&format!("${{{k}}}"), v)
        });

        let resolved_dep = resolve_dependency(dep, &version, local_repo)?;
        resolved.push(resolved_dep);
    }

    Ok(resolved)
}

fn resolve_dependency(
    dep: &Dependency,
    version: &str,
    local_repo: &Path,
) -> Result<ResolvedDependency> {
    let jar_path = artifact_path(local_repo, &dep.group_id, &dep.artifact_id, version, "jar");

    if !jar_path.exists() {
        download_artifact(&dep.group_id, &dep.artifact_id, version, local_repo).with_context(
            || {
                format!(
                    "Failed to download {}:{}:{}",
                    dep.group_id, dep.artifact_id, version
                )
            },
        )?;
    }

    Ok(ResolvedDependency {
        path: jar_path,
        group_id: dep.group_id.clone(),
        artifact_id: dep.artifact_id.clone(),
        version: version.to_string(),
    })
}

fn remote_artifact_url(
    group_id: &str,
    artifact_id: &str,
    version: &str,
    extension: &str,
) -> String {
    let group_path = group_id.replace('.', "/");
    format!(
        "{MAVEN_CENTRAL}/{group_path}/{artifact_id}/{version}/{artifact_id}-{version}.{extension}"
    )
}

fn download_artifact(
    group_id: &str,
    artifact_id: &str,
    version: &str,
    local_repo: &Path,
) -> Result<()> {
    validate_artifact_components(group_id, artifact_id, version)?;

    let jar_path = artifact_path(local_repo, group_id, artifact_id, version, "jar");
    let sha1_path = artifact_path(local_repo, group_id, artifact_id, version, "jar.sha1");

    if let Some(parent) = jar_path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
    }

    let jar_url = remote_artifact_url(group_id, artifact_id, version, "jar");
    let sha1_url = remote_artifact_url(group_id, artifact_id, version, "jar.sha1");

    println!("[INFO] Downloading {jar_url}");

    // Download SHA1 first
    let sha1_response = reqwest::blocking::get(&sha1_url)
        .with_context(|| format!("Failed to fetch SHA1 from {sha1_url}"))?;
    if !sha1_response.status().is_success() {
        anyhow::bail!("SHA1 not found at {sha1_url}: {}", sha1_response.status());
    }
    let expected_sha1_raw = sha1_response
        .text()
        .with_context(|| "Failed to read SHA1 response")?;
    // split_whitespace already trims leading/trailing whitespace
    let expected_sha1 = expected_sha1_raw
        .split_whitespace()
        .next()
        .unwrap_or("")
        .to_lowercase();

    // Download JAR
    let jar_response = reqwest::blocking::get(&jar_url)
        .with_context(|| format!("Failed to fetch JAR from {jar_url}"))?;
    if !jar_response.status().is_success() {
        anyhow::bail!("JAR not found at {jar_url}: {}", jar_response.status());
    }
    let jar_bytes = jar_response
        .bytes()
        .with_context(|| "Failed to read JAR response bytes")?;

    // Verify checksum
    let mut hasher = Sha1::new();
    hasher.update(&jar_bytes);
    let actual_sha1 = hex::encode(hasher.finalize());

    if !expected_sha1.is_empty() && actual_sha1 != expected_sha1 {
        anyhow::bail!(
            "SHA1 checksum mismatch for {group_id}:{artifact_id}:{version}: \
             expected {expected_sha1}, got {actual_sha1}"
        );
    }

    std::fs::write(&jar_path, &jar_bytes)
        .with_context(|| format!("Failed to write JAR to {}", jar_path.display()))?;
    std::fs::write(&sha1_path, &actual_sha1)
        .with_context(|| format!("Failed to write SHA1 to {}", sha1_path.display()))?;

    println!("[INFO] Downloaded {jar_url} to {}", jar_path.display());
    Ok(())
}

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

    #[test]
    fn test_remote_artifact_url() {
        let url = remote_artifact_url("com.example", "my-lib", "1.0.0", "jar");
        assert_eq!(
            url,
            "https://repo1.maven.org/maven2/com/example/my-lib/1.0.0/my-lib-1.0.0.jar"
        );
    }

    #[test]
    fn test_path_generation() {
        let repo = Path::new("/repo");
        let path = artifact_path(repo, "junit", "junit", "4.13.2", "jar");
        assert_eq!(
            path,
            PathBuf::from("/repo/junit/junit/4.13.2/junit-4.13.2.jar")
        );
    }

    #[test]
    fn test_resolve_skips_optional() {
        use crate::pom::{DependenciesWrapper, Dependency, Pom};

        let pom = Pom {
            group_id: "com.example".to_string(),
            artifact_id: "test".to_string(),
            version: "1.0".to_string(),
            dependencies: DependenciesWrapper {
                items: vec![Dependency {
                    group_id: "some".to_string(),
                    artifact_id: "dep".to_string(),
                    version: "1.0".to_string(),
                    optional: true,
                    ..Dependency::default()
                }],
            },
            ..Pom::default()
        };

        let repo = Path::new("/nonexistent/repo");
        // Should return empty because the dependency is optional
        let resolved = resolve_dependencies(&pom, repo, &HashMap::new()).unwrap();
        assert!(resolved.is_empty());
    }

    #[test]
    fn test_resolve_skips_provided_scope() {
        use crate::pom::{DependenciesWrapper, Dependency, Pom};

        let pom = Pom {
            group_id: "com.example".to_string(),
            artifact_id: "test".to_string(),
            version: "1.0".to_string(),
            dependencies: DependenciesWrapper {
                items: vec![Dependency {
                    group_id: "javax.servlet".to_string(),
                    artifact_id: "servlet-api".to_string(),
                    version: "2.5".to_string(),
                    scope: "provided".to_string(),
                    ..Dependency::default()
                }],
            },
            ..Pom::default()
        };

        let repo = Path::new("/nonexistent/repo");
        let resolved = resolve_dependencies(&pom, repo, &HashMap::new()).unwrap();
        assert!(resolved.is_empty());
    }
}
