//! Cargo.lock file parsing.
//!
//! Parses Cargo.lock files (version 3 and 4) to extract resolved dependency
//! versions. Supports workspace lock files and proper path resolution.
//!
//! # Cargo.lock Format
//!
//! Cargo.lock uses TOML format with an array of packages:
//!
//! ```toml
//! # This file is automatically @generated by Cargo.
//! # It is not intended for manual editing.
//! version = 4
//!
//! [[package]]
//! name = "serde"
//! version = "1.0.195"
//! source = "registry+https://github.com/rust-lang/crates.io-index"
//! checksum = "..."
//! dependencies = [
//!     "serde_derive",
//! ]
//! ```

use deps_core::error::{DepsError, Result};
use deps_core::lockfile::{
    LockFileProvider, ResolvedPackage, ResolvedPackages, ResolvedSource,
    locate_lockfile_for_manifest,
};
use std::path::{Path, PathBuf};
use tower_lsp_server::ls_types::Uri;

/// Cargo.lock file parser.
///
/// Implements lock file parsing for Rust's Cargo build system.
/// Supports both project-level and workspace-level lock files.
///
/// # Lock File Location
///
/// The parser searches for Cargo.lock in the following order:
/// 1. Same directory as Cargo.toml
/// 2. Parent directories (up to 5 levels) for workspace root
///
/// # Examples
///
/// ```no_run
/// use deps_cargo::lockfile::CargoLockParser;
/// use deps_core::lockfile::LockFileProvider;
/// use tower_lsp_server::ls_types::Uri;
///
/// # async fn example() -> deps_core::error::Result<()> {
/// let parser = CargoLockParser;
/// let manifest_uri = Uri::from_file_path("/path/to/Cargo.toml").unwrap();
///
/// if let Some(lockfile_path) = parser.locate_lockfile(&manifest_uri) {
///     let resolved = parser.parse_lockfile(&lockfile_path).await?;
///     println!("Found {} resolved packages", resolved.len());
/// }
/// # Ok(())
/// # }
/// ```
pub struct CargoLockParser;

impl CargoLockParser {
    /// Lock file names for Cargo ecosystem.
    const LOCKFILE_NAMES: &'static [&'static str] = &["Cargo.lock"];
}

impl LockFileProvider for CargoLockParser {
    fn locate_lockfile(&self, manifest_uri: &Uri) -> Option<PathBuf> {
        locate_lockfile_for_manifest(manifest_uri, Self::LOCKFILE_NAMES)
    }

    fn parse_lockfile<'a>(
        &'a self,
        lockfile_path: &'a Path,
    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ResolvedPackages>> + Send + 'a>>
    {
        Box::pin(async move {
            tracing::debug!("Parsing Cargo.lock: {}", lockfile_path.display());

            let content = tokio::fs::read_to_string(lockfile_path)
                .await
                .map_err(|e| DepsError::ParseError {
                    file_type: format!("Cargo.lock at {}", lockfile_path.display()),
                    source: Box::new(e),
                })?;

            let doc = toml_span::parse(&content).map_err(|e| DepsError::ParseError {
                file_type: "Cargo.lock".into(),
                source: Box::new(std::io::Error::other(e.to_string())),
            })?;

            let mut packages = ResolvedPackages::new();

            let Some(root_table) = doc.as_table() else {
                tracing::warn!("Cargo.lock root is not a table");
                return Ok(packages);
            };

            let Some(package_array_val) = root_table.get("package") else {
                tracing::warn!("Cargo.lock missing [[package]] array of tables");
                return Ok(packages);
            };

            let Some(package_array) = package_array_val.as_array() else {
                tracing::warn!("Cargo.lock [[package]] is not an array");
                return Ok(packages);
            };

            for entry in package_array {
                let Some(table) = entry.as_table() else {
                    continue;
                };

                // Extract required fields
                let Some(name) = table.get("name").and_then(|v| v.as_str()) else {
                    tracing::warn!("Package missing name field");
                    continue;
                };

                let Some(version) = table.get("version").and_then(|v| v.as_str()) else {
                    tracing::warn!("Package '{}' missing version field", name);
                    continue;
                };

                // Parse source (optional for path dependencies)
                let source = parse_cargo_source(table.get("source").and_then(|v| v.as_str()));

                // Parse dependencies array (optional)
                let dependencies = parse_cargo_dependencies_from_table(table);

                packages.insert(ResolvedPackage {
                    name: name.to_string(),
                    version: version.to_string(),
                    source,
                    dependencies,
                });
            }

            tracing::info!(
                "Parsed Cargo.lock: {} packages from {}",
                packages.len(),
                lockfile_path.display()
            );

            Ok(packages)
        })
    }
}

/// Parses Cargo source field into ResolvedSource.
///
/// # Source Formats
///
/// - `"registry+https://github.com/rust-lang/crates.io-index"` → Registry
/// - `"git+https://github.com/user/repo#commit"` → Git
/// - None (path dependencies don't have source field) → Path
fn parse_cargo_source(source_str: Option<&str>) -> ResolvedSource {
    let Some(source) = source_str else {
        return ResolvedSource::Path {
            path: String::new(),
        };
    };

    if let Some(registry_url) = source.strip_prefix("registry+") {
        ResolvedSource::Registry {
            url: registry_url.to_string(),
            checksum: String::new(),
        }
    } else if let Some(git_part) = source.strip_prefix("git+") {
        let (url, rev) = if let Some((u, r)) = git_part.split_once('#') {
            (u.to_string(), r.to_string())
        } else {
            (git_part.to_string(), String::new())
        };

        ResolvedSource::Git { url, rev }
    } else {
        ResolvedSource::Path {
            path: source.to_string(),
        }
    }
}

/// Parses dependencies array from package table.
///
/// Dependencies are typically simple strings in Cargo.lock v4:
/// ```toml
/// dependencies = ["serde_derive", "syn"]
/// ```
fn parse_cargo_dependencies_from_table(table: &toml_span::value::Table<'_>) -> Vec<String> {
    let Some(deps_value) = table.get("dependencies") else {
        return vec![];
    };

    let Some(deps_array) = deps_value.as_array() else {
        return vec![];
    };

    deps_array
        .iter()
        .filter_map(|item| {
            // Simple string format (most common)
            if let Some(s) = item.as_str() {
                return Some(s.to_string());
            }

            // Table format (rare, extract "name" field)
            if let Some(t) = item.as_table()
                && let Some(name) = t.get("name").and_then(|v| v.as_str())
            {
                return Some(name.to_string());
            }

            None
        })
        .collect()
}

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

    #[test]
    fn test_parse_cargo_source_registry() {
        let source = parse_cargo_source(Some(
            "registry+https://github.com/rust-lang/crates.io-index",
        ));

        match source {
            ResolvedSource::Registry { url, .. } => {
                assert_eq!(url, "https://github.com/rust-lang/crates.io-index");
            }
            _ => panic!("Expected Registry source"),
        }
    }

    #[test]
    fn test_parse_cargo_source_git() {
        let source = parse_cargo_source(Some("git+https://github.com/user/repo#abc123"));

        match source {
            ResolvedSource::Git { url, rev } => {
                assert_eq!(url, "https://github.com/user/repo");
                assert_eq!(rev, "abc123");
            }
            _ => panic!("Expected Git source"),
        }
    }

    #[test]
    fn test_parse_cargo_source_git_no_commit() {
        let source = parse_cargo_source(Some("git+https://github.com/user/repo"));

        match source {
            ResolvedSource::Git { url, rev } => {
                assert_eq!(url, "https://github.com/user/repo");
                assert!(rev.is_empty());
            }
            _ => panic!("Expected Git source"),
        }
    }

    #[test]
    fn test_parse_cargo_source_path() {
        let source = parse_cargo_source(None);

        match source {
            ResolvedSource::Path { path } => {
                assert!(path.is_empty());
            }
            _ => panic!("Expected Path source"),
        }
    }

    #[tokio::test]
    async fn test_parse_simple_cargo_lock() {
        let lockfile_content = r#"
# This file is automatically @generated by Cargo.
version = 4

[[package]]
name = "serde"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc123"
dependencies = [
    "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "def456"
"#;

        let temp_dir = tempfile::tempdir().unwrap();
        let lockfile_path = temp_dir.path().join("Cargo.lock");
        std::fs::write(&lockfile_path, lockfile_content).unwrap();

        let parser = CargoLockParser;
        let resolved = parser.parse_lockfile(&lockfile_path).await.unwrap();

        assert_eq!(resolved.len(), 2);
        assert_eq!(resolved.get_version("serde"), Some("1.0.195"));
        assert_eq!(resolved.get_version("serde_derive"), Some("1.0.195"));

        let serde_pkg = resolved.get("serde").unwrap();
        assert_eq!(serde_pkg.dependencies.len(), 1);
        assert_eq!(serde_pkg.dependencies[0], "serde_derive");
    }

    #[tokio::test]
    async fn test_parse_cargo_lock_with_git() {
        let lockfile_content = r#"
version = 4

[[package]]
name = "my-git-dep"
version = "0.1.0"
source = "git+https://github.com/user/repo#abc123"
"#;

        let temp_dir = tempfile::tempdir().unwrap();
        let lockfile_path = temp_dir.path().join("Cargo.lock");
        std::fs::write(&lockfile_path, lockfile_content).unwrap();

        let parser = CargoLockParser;
        let resolved = parser.parse_lockfile(&lockfile_path).await.unwrap();

        assert_eq!(resolved.len(), 1);
        let pkg = resolved.get("my-git-dep").unwrap();
        assert_eq!(pkg.version, "0.1.0");

        match &pkg.source {
            ResolvedSource::Git { url, rev } => {
                assert_eq!(url, "https://github.com/user/repo");
                assert_eq!(rev, "abc123");
            }
            _ => panic!("Expected Git source"),
        }
    }

    #[tokio::test]
    async fn test_parse_empty_cargo_lock() {
        let lockfile_content = r"
version = 4
";

        let temp_dir = tempfile::tempdir().unwrap();
        let lockfile_path = temp_dir.path().join("Cargo.lock");
        std::fs::write(&lockfile_path, lockfile_content).unwrap();

        let parser = CargoLockParser;
        let resolved = parser.parse_lockfile(&lockfile_path).await.unwrap();

        assert_eq!(resolved.len(), 0);
        assert!(resolved.is_empty());
    }

    #[tokio::test]
    async fn test_parse_malformed_cargo_lock() {
        let lockfile_content = "not valid toml {{{";

        let temp_dir = tempfile::tempdir().unwrap();
        let lockfile_path = temp_dir.path().join("Cargo.lock");
        std::fs::write(&lockfile_path, lockfile_content).unwrap();

        let parser = CargoLockParser;
        let result = parser.parse_lockfile(&lockfile_path).await;

        assert!(result.is_err());
    }

    #[test]
    fn test_locate_lockfile_same_directory() {
        let temp_dir = tempfile::tempdir().unwrap();
        let manifest_path = temp_dir.path().join("Cargo.toml");
        let lock_path = temp_dir.path().join("Cargo.lock");

        std::fs::write(&manifest_path, "[package]\nname = \"test\"").unwrap();
        std::fs::write(&lock_path, "version = 4").unwrap();

        let manifest_uri = Uri::from_file_path(&manifest_path).unwrap();
        let parser = CargoLockParser;

        let located = parser.locate_lockfile(&manifest_uri);
        assert!(located.is_some());
        assert_eq!(located.unwrap(), lock_path);
    }

    #[test]
    fn test_locate_lockfile_workspace_root() {
        let temp_dir = tempfile::tempdir().unwrap();
        let workspace_lock = temp_dir.path().join("Cargo.lock");
        let member_dir = temp_dir.path().join("crates").join("member");
        std::fs::create_dir_all(&member_dir).unwrap();
        let member_manifest = member_dir.join("Cargo.toml");

        std::fs::write(&workspace_lock, "version = 4").unwrap();
        std::fs::write(&member_manifest, "[package]\nname = \"member\"").unwrap();

        let manifest_uri = Uri::from_file_path(&member_manifest).unwrap();
        let parser = CargoLockParser;

        let located = parser.locate_lockfile(&manifest_uri);
        assert!(located.is_some());
        assert_eq!(located.unwrap(), workspace_lock);
    }

    #[test]
    fn test_locate_lockfile_not_found() {
        let temp_dir = tempfile::tempdir().unwrap();
        let manifest_path = temp_dir.path().join("Cargo.toml");
        std::fs::write(&manifest_path, "[package]\nname = \"test\"").unwrap();

        let manifest_uri = Uri::from_file_path(&manifest_path).unwrap();
        let parser = CargoLockParser;

        let located = parser.locate_lockfile(&manifest_uri);
        assert!(located.is_none());
    }

    #[test]
    fn test_is_lockfile_stale_not_modified() {
        let temp_dir = tempfile::tempdir().unwrap();
        let lockfile_path = temp_dir.path().join("Cargo.lock");
        std::fs::write(&lockfile_path, "version = 4").unwrap();

        let mtime = std::fs::metadata(&lockfile_path)
            .unwrap()
            .modified()
            .unwrap();
        let parser = CargoLockParser;

        assert!(
            !parser.is_lockfile_stale(&lockfile_path, mtime),
            "Lock file should not be stale when mtime matches"
        );
    }

    #[test]
    fn test_is_lockfile_stale_modified() {
        let temp_dir = tempfile::tempdir().unwrap();
        let lockfile_path = temp_dir.path().join("Cargo.lock");
        std::fs::write(&lockfile_path, "version = 4").unwrap();

        let old_time = std::time::UNIX_EPOCH;
        let parser = CargoLockParser;

        assert!(
            parser.is_lockfile_stale(&lockfile_path, old_time),
            "Lock file should be stale when last_modified is old"
        );
    }

    #[test]
    fn test_is_lockfile_stale_deleted() {
        let parser = CargoLockParser;
        let non_existent = std::path::Path::new("/nonexistent/Cargo.lock");

        assert!(
            parser.is_lockfile_stale(non_existent, std::time::SystemTime::now()),
            "Non-existent lock file should be considered stale"
        );
    }

    #[test]
    fn test_is_lockfile_stale_future_time() {
        let temp_dir = tempfile::tempdir().unwrap();
        let lockfile_path = temp_dir.path().join("Cargo.lock");
        std::fs::write(&lockfile_path, "version = 4").unwrap();

        // Use a time far in the future
        let future_time = std::time::SystemTime::now() + std::time::Duration::from_secs(86400); // +1 day
        let parser = CargoLockParser;

        assert!(
            !parser.is_lockfile_stale(&lockfile_path, future_time),
            "Lock file should not be stale when last_modified is in the future"
        );
    }
}
