//! Lock file (sage.lock) management.

use crate::error::PackageError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;

/// The lock file format for sage.lock.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LockFile {
    /// Schema version for future compatibility.
    #[serde(default = "default_version")]
    pub version: u32,
    /// All locked packages (including transitive dependencies).
    #[serde(default)]
    pub packages: Vec<LockedPackage>,
}

fn default_version() -> u32 {
    1
}

/// A locked package entry.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LockedPackage {
    /// Package name (as declared in sage.toml).
    pub name: String,
    /// Package version from its sage.toml.
    pub version: String,
    /// Git repository URL.
    pub git: String,
    /// Pinned full SHA.
    pub rev: String,
    /// List of package names this depends on (for ordering).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub dependencies: Vec<String>,
}

impl LockFile {
    /// Load a lock file from disk.
    pub fn load(path: &Path) -> Result<Self, PackageError> {
        let contents = std::fs::read_to_string(path).map_err(|e| PackageError::IoError {
            message: format!("failed to read {}", path.display()),
            source: e,
        })?;

        toml::from_str(&contents).map_err(|e| PackageError::InvalidLockFile { source: e })
    }

    /// Save the lock file to disk.
    pub fn save(&self, path: &Path) -> Result<(), PackageError> {
        let header = "# This file is auto-generated by sage. Do not edit manually.\n\n";
        let contents = toml::to_string_pretty(self).map_err(|e| PackageError::IoError {
            message: format!("failed to serialize lock file: {e}"),
            source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
        })?;

        std::fs::write(path, format!("{header}{contents}")).map_err(|e| PackageError::IoError {
            message: format!("failed to write {}", path.display()),
            source: e,
        })?;

        Ok(())
    }

    /// Check if the lock file is empty.
    pub fn is_empty(&self) -> bool {
        self.packages.is_empty()
    }

    /// Find a package by name.
    pub fn find(&self, name: &str) -> Option<&LockedPackage> {
        self.packages.iter().find(|p| p.name == name)
    }

    /// Build a map of package name to locked package.
    pub fn package_map(&self) -> HashMap<&str, &LockedPackage> {
        self.packages.iter().map(|p| (p.name.as_str(), p)).collect()
    }

    /// Check if the lock file matches the given dependencies.
    pub fn matches_dependencies(&self, deps: &HashMap<String, crate::DependencySpec>) -> bool {
        // All deps must be in lock file with matching git URL
        for (name, spec) in deps {
            match self.find(name) {
                Some(locked) => {
                    if locked.git != spec.git {
                        return false;
                    }
                }
                None => return false,
            }
        }
        true
    }

    /// Return packages in dependency order (dependencies first).
    pub fn in_dependency_order(&self) -> Vec<&LockedPackage> {
        // Simple topological sort
        let mut result = Vec::new();
        let mut visited = std::collections::HashSet::new();
        let pkg_map: HashMap<&str, &LockedPackage> = self.package_map();

        fn visit<'a>(
            pkg: &'a LockedPackage,
            pkg_map: &HashMap<&str, &'a LockedPackage>,
            visited: &mut std::collections::HashSet<&'a str>,
            result: &mut Vec<&'a LockedPackage>,
        ) {
            if visited.contains(pkg.name.as_str()) {
                return;
            }
            visited.insert(&pkg.name);

            for dep_name in &pkg.dependencies {
                if let Some(dep) = pkg_map.get(dep_name.as_str()) {
                    visit(dep, pkg_map, visited, result);
                }
            }
            result.push(pkg);
        }

        for pkg in &self.packages {
            visit(pkg, &pkg_map, &mut visited, &mut result);
        }

        result
    }
}

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

    #[test]
    fn serialize_lock_file() {
        let lock = LockFile {
            version: 1,
            packages: vec![
                LockedPackage {
                    name: "foo".to_string(),
                    version: "1.0.0".to_string(),
                    git: "https://github.com/example/foo".to_string(),
                    rev: "abc123def456".to_string(),
                    dependencies: vec![],
                },
                LockedPackage {
                    name: "bar".to_string(),
                    version: "2.0.0".to_string(),
                    git: "https://github.com/example/bar".to_string(),
                    rev: "789xyz".to_string(),
                    dependencies: vec!["foo".to_string()],
                },
            ],
        };

        let serialized = toml::to_string_pretty(&lock).unwrap();
        assert!(serialized.contains("name = \"foo\""));
        assert!(serialized.contains("name = \"bar\""));
        assert!(serialized.contains("dependencies = [\"foo\"]"));
    }

    #[test]
    fn deserialize_lock_file() {
        let toml_str = r#"
version = 1

[[packages]]
name = "foo"
version = "1.0.0"
git = "https://github.com/example/foo"
rev = "abc123"

[[packages]]
name = "bar"
version = "2.0.0"
git = "https://github.com/example/bar"
rev = "def456"
dependencies = ["foo"]
"#;

        let lock: LockFile = toml::from_str(toml_str).unwrap();
        assert_eq!(lock.packages.len(), 2);
        assert_eq!(lock.packages[0].name, "foo");
        assert_eq!(lock.packages[1].dependencies, vec!["foo"]);
    }

    #[test]
    fn find_package() {
        let lock = LockFile {
            version: 1,
            packages: vec![LockedPackage {
                name: "test".to_string(),
                version: "1.0.0".to_string(),
                git: "https://example.com/test".to_string(),
                rev: "abc".to_string(),
                dependencies: vec![],
            }],
        };

        assert!(lock.find("test").is_some());
        assert!(lock.find("nonexistent").is_none());
    }

    #[test]
    fn dependency_order() {
        let lock = LockFile {
            version: 1,
            packages: vec![
                LockedPackage {
                    name: "c".to_string(),
                    version: "1.0.0".to_string(),
                    git: "https://example.com/c".to_string(),
                    rev: "ccc".to_string(),
                    dependencies: vec!["a".to_string(), "b".to_string()],
                },
                LockedPackage {
                    name: "a".to_string(),
                    version: "1.0.0".to_string(),
                    git: "https://example.com/a".to_string(),
                    rev: "aaa".to_string(),
                    dependencies: vec![],
                },
                LockedPackage {
                    name: "b".to_string(),
                    version: "1.0.0".to_string(),
                    git: "https://example.com/b".to_string(),
                    rev: "bbb".to_string(),
                    dependencies: vec!["a".to_string()],
                },
            ],
        };

        let ordered = lock.in_dependency_order();
        let names: Vec<&str> = ordered.iter().map(|p| p.name.as_str()).collect();

        // a must come before b and c, b must come before c
        let a_pos = names.iter().position(|&n| n == "a").unwrap();
        let b_pos = names.iter().position(|&n| n == "b").unwrap();
        let c_pos = names.iter().position(|&n| n == "c").unwrap();

        assert!(a_pos < b_pos);
        assert!(a_pos < c_pos);
        assert!(b_pos < c_pos);
    }
}
