use std::collections::HashSet;
use std::path::{Path, PathBuf};

use crate::args::RustcArgs;
use guppy::graph::{DependencyDirection, PackageGraph};
use kache_core::BuildIntent;

struct MetadataDiscovery {
    crate_names: Vec<String>,
    workspace_root: Option<PathBuf>,
}

pub fn discover(args: Option<&RustcArgs>) -> Option<BuildIntent> {
    let metadata = discover_metadata(args)?;
    let crate_names = metadata.crate_names;
    if crate_names.is_empty() {
        return None;
    }

    let namespace = std::env::var("KACHE_NAMESPACE")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty());

    let lock_path = metadata
        .workspace_root
        .as_deref()
        .map(|root| root.join("Cargo.lock"))
        .unwrap_or_else(|| PathBuf::from("Cargo.lock"));
    let cargo_lock_deps = namespace
        .as_ref()
        .and_then(|_| load_cargo_lock_deps(&lock_path))
        .unwrap_or_default();

    Some(BuildIntent {
        crate_names,
        namespace,
        cargo_lock_deps,
    })
}

pub fn into_build_started_request(
    intent: BuildIntent,
    client_epoch: u64,
) -> crate::daemon::BuildStartedRequest {
    crate::daemon::BuildStartedRequest {
        intent,
        client_epoch,
    }
}

fn load_cargo_lock_deps(lock_path: &Path) -> Option<Vec<(String, String)>> {
    crate::shards::parse_cargo_lock(lock_path)
        .map_err(|err| {
            tracing::debug!(
                "build intent: failed to parse {} for shard prefetch: {}",
                lock_path.display(),
                err
            );
            err
        })
        .ok()
}

fn discover_metadata(args: Option<&RustcArgs>) -> Option<MetadataDiscovery> {
    for manifest_path in candidate_manifest_paths(args) {
        if let Some(discovery) = run_cargo_metadata(Some(&manifest_path)) {
            return Some(discovery);
        }
    }

    run_cargo_metadata(None)
}

fn candidate_manifest_paths(args: Option<&RustcArgs>) -> Vec<PathBuf> {
    let mut candidates = Vec::new();

    if let Some(out_dir) = args.and_then(|a| a.out_dir.as_deref())
        && let Some(path) = manifest_from_target_out_dir(out_dir)
        && path.is_file()
    {
        candidates.push(path);
    }

    if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
        let path = PathBuf::from(manifest_dir).join("Cargo.toml");
        if path.is_file() && !candidates.iter().any(|existing| existing == &path) {
            candidates.push(path);
        }
    }

    candidates
}

fn manifest_from_target_out_dir(out_dir: &Path) -> Option<PathBuf> {
    for ancestor in out_dir.ancestors() {
        if ancestor.file_name().and_then(|name| name.to_str()) == Some("target") {
            return ancestor.parent().map(|root| root.join("Cargo.toml"));
        }
    }
    None
}

fn run_cargo_metadata(manifest_path: Option<&Path>) -> Option<MetadataDiscovery> {
    let mut command = std::process::Command::new("cargo");
    command
        .args(["metadata", "--format-version", "1"])
        .env_remove("RUSTC_WRAPPER")
        .env_remove("RUSTC_WORKSPACE_WRAPPER")
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::null());

    if let Some(path) = manifest_path {
        command.arg("--manifest-path").arg(path);
    }

    let output = command.output().ok()?;

    if !output.status.success() {
        return None;
    }

    parse_metadata_graph(&output.stdout, manifest_path)
}

fn parse_metadata_graph(
    metadata_json: &[u8],
    manifest_path: Option<&Path>,
) -> Option<MetadataDiscovery> {
    let graph = PackageGraph::from_json(std::str::from_utf8(metadata_json).ok()?).ok()?;
    let crate_names = graph_crate_order(&graph, manifest_path)?;
    let workspace_root = Some(graph.workspace().root().as_std_path().to_path_buf());

    Some(MetadataDiscovery {
        crate_names,
        workspace_root,
    })
}

fn graph_crate_order(graph: &PackageGraph, manifest_path: Option<&Path>) -> Option<Vec<String>> {
    let package_ids = if let Some(manifest_path) = manifest_path
        && let Some(package) = graph
            .packages()
            .find(|package| paths_match(package.manifest_path().as_std_path(), manifest_path))
    {
        vec![package.id().clone()]
    } else {
        graph
            .workspace()
            .iter()
            .map(|package| package.id().clone())
            .collect::<Vec<_>>()
    };

    let package_set = graph.query_forward(package_ids.iter()).ok()?.resolve();
    let mut seen = HashSet::new();
    let mut ordered = Vec::new();

    for package in package_set.packages(DependencyDirection::Reverse) {
        let name = package.name().to_string();
        if seen.insert(name.clone()) {
            ordered.push(name);
        }
    }

    Some(ordered)
}

fn paths_match(left: &Path, right: &Path) -> bool {
    if left == right {
        return true;
    }

    match (std::fs::canonicalize(left), std::fs::canonicalize(right)) {
        (Ok(left), Ok(right)) => left == right,
        _ => false,
    }
}

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

    #[test]
    fn test_run_cargo_metadata_uses_guppy_graph_and_workspace_root() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();

        std::fs::write(
            root.join("Cargo.toml"),
            r#"[workspace]
members = ["app", "dep", "unrelated"]
resolver = "2"
"#,
        )
        .unwrap();

        std::fs::create_dir_all(root.join("app/src")).unwrap();
        std::fs::write(
            root.join("app/Cargo.toml"),
            r#"[package]
name = "app"
version = "0.1.0"
edition = "2021"

[dependencies]
dep = { path = "../dep" }
"#,
        )
        .unwrap();
        std::fs::write(root.join("app/src/lib.rs"), "").unwrap();

        std::fs::create_dir_all(root.join("dep/src")).unwrap();
        std::fs::write(
            root.join("dep/Cargo.toml"),
            r#"[package]
name = "dep"
version = "0.1.0"
edition = "2021"
"#,
        )
        .unwrap();
        std::fs::write(root.join("dep/src/lib.rs"), "").unwrap();

        std::fs::create_dir_all(root.join("unrelated/src")).unwrap();
        std::fs::write(
            root.join("unrelated/Cargo.toml"),
            r#"[package]
name = "unrelated"
version = "0.1.0"
edition = "2021"
"#,
        )
        .unwrap();
        std::fs::write(root.join("unrelated/src/lib.rs"), "").unwrap();

        let discovery = run_cargo_metadata(Some(&root.join("app/Cargo.toml"))).unwrap();
        assert_eq!(discovery.crate_names, vec!["dep", "app"]);
        assert_eq!(discovery.workspace_root.as_deref(), Some(root));
    }

    #[test]
    fn test_manifest_from_target_out_dir_uses_target_parent() {
        let manifest = manifest_from_target_out_dir(Path::new(
            "/repo/apps/tauri/src-tauri/target/release/deps",
        ))
        .unwrap();

        assert_eq!(manifest, Path::new("/repo/apps/tauri/src-tauri/Cargo.toml"));
    }

    #[test]
    fn test_build_intent_into_request_preserves_shard_context() {
        let intent = BuildIntent {
            crate_names: vec!["serde".into(), "tokio".into()],
            namespace: Some("x86_64/hash/release".into()),
            cargo_lock_deps: vec![("serde".into(), "1.0.0".into())],
        };

        let req = into_build_started_request(intent, 42);
        assert_eq!(req.intent.crate_names, vec!["serde", "tokio"]);
        assert_eq!(req.intent.namespace.as_deref(), Some("x86_64/hash/release"));
        assert_eq!(req.intent.cargo_lock_deps.len(), 1);
        assert_eq!(req.client_epoch, 42);
    }
}
