//! LFS operations on a Git repository.

use std::fs;
use std::path::{Path, PathBuf};

use globset::Glob;

use crate::commit::types::CommitOid;
use crate::lfs::types::{LfsConfig, LfsEntry, LfsOid, LfsPointer};
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn lfs_pointer_from_blob(&self, oid: &CommitOid) -> GitResult<Option<LfsPointer>> {
        let content = self.blob_content(oid)?;
        Ok(LfsPointer::from_bytes(&content.content))
    }

    pub fn lfs_is_pointer(&self, oid: &CommitOid) -> GitResult<bool> {
        let content = self.blob_content(oid)?;
        Ok(LfsPointer::from_bytes(&content.content).is_some())
    }

    pub fn lfs_resolve_oid(&self, oid: &CommitOid) -> GitResult<Option<LfsOid>> {
        let pointer = self.lfs_pointer_from_blob(oid)?;
        Ok(pointer.map(|p| p.oid))
    }

    pub fn lfs_create_pointer(
        &self,
        oid: &LfsOid,
        size: u64,
        extra: &[(String, String)],
    ) -> GitResult<CommitOid> {
        let pointer = LfsPointer {
            version: "https://git-lfs.github.com/spec/v1".to_string(),
            oid: oid.clone(),
            size,
            extra: extra.to_vec(),
        };
        self.blob_create_from_string(&pointer.to_string())
    }

    pub fn lfs_scan_tree(&self, tree_oid: &CommitOid, recursive: bool) -> GitResult<Vec<LfsEntry>> {
        let tree_oid = tree_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(tree_oid.to_string()))?;
        let tree = self
            .repo()
            .find_tree(tree_oid)
            .map_err(|_| GitError::ObjectNotFound(tree_oid.to_string()))?;
        let mut entries = Vec::new();
        self.lfs_scan_tree_impl(&mut entries, &tree, "", recursive)?;
        Ok(entries)
    }

    fn lfs_scan_tree_impl(
        &self,
        out: &mut Vec<LfsEntry>,
        tree: &git2::Tree<'_>,
        prefix: &str,
        recursive: bool,
    ) -> GitResult<()> {
        for entry in tree.iter() {
            let name = entry.name().unwrap_or("");
            let full_path = if prefix.is_empty() {
                name.to_string()
            } else {
                format!("{}/{}", prefix, name)
            };

            let blob_oid = entry.id();
            let obj = match self.repo().find_object(blob_oid, None) {
                Ok(o) => o,
                Err(_) => continue,
            };

            if obj.kind() == Some(git2::ObjectType::Tree) {
                if recursive {
                    let sub_tree = self
                        .repo()
                        .find_tree(blob_oid)
                        .map_err(|e| GitError::Internal(e.to_string()))?;
                    self.lfs_scan_tree_impl(out, &sub_tree, &full_path, recursive)?;
                }
            } else if let Some(blob) = obj.as_blob() {
                if let Some(pointer) = LfsPointer::from_bytes(blob.content()) {
                    out.push(LfsEntry {
                        path: full_path,
                        pointer,
                        size: 0,
                    });
                }
            }
        }
        Ok(())
    }

    fn gitattributes_path(&self) -> PathBuf {
        self.repo()
            .workdir()
            .unwrap_or_else(|| self.repo().path())
            .join(".gitattributes")
    }

    pub fn lfs_gitattributes_list(&self) -> GitResult<Vec<String>> {
        let path = self.gitattributes_path();
        if !path.exists() {
            return Ok(Vec::new());
        }
        let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?;
        Ok(content
            .lines()
            .filter(|l| l.contains("filter=lfs"))
            .map(|l| l.trim().to_string())
            .collect())
    }

    pub fn lfs_gitattributes_add(&self, pattern: &str) -> GitResult<()> {
        let line = format!("{} filter=lfs diff=lfs merge=lfs -text", pattern);
        let path = self.gitattributes_path();

        let content = if path.exists() {
            fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?
        } else {
            String::new()
        };

        if content.lines().any(|l| l.trim() == line) {
            return Ok(());
        }

        let new_content = if content.ends_with('\n') || content.is_empty() {
            format!("{}{}\n", content, line)
        } else {
            format!("{}\n{}\n", content, line)
        };

        fs::write(&path, new_content).map_err(|e| GitError::IoError(e.to_string()))?;
        Ok(())
    }

    pub fn lfs_gitattributes_remove(&self, pattern: &str) -> GitResult<bool> {
        let path = self.gitattributes_path();
        if !path.exists() {
            return Ok(false);
        }
        let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?;
        let target = format!("{} filter=lfs diff=lfs merge=lfs -text", pattern);
        let new_lines: Vec<&str> = content.lines().filter(|l| l.trim() != target).collect();

        if new_lines.len() == content.lines().count() {
            return Ok(false);
        }

        let new_content = new_lines.join("\n");
        fs::write(&path, new_content).map_err(|e| GitError::IoError(e.to_string()))?;
        Ok(true)
    }

    pub fn lfs_gitattributes_match(&self, path_str: &str) -> GitResult<bool> {
        let patterns = self.lfs_gitattributes_list()?;
        for pattern in patterns {
            let glob_str = pattern.split_whitespace().next().unwrap_or(&pattern);
            if let Ok(glob) = Glob::new(glob_str) {
                let matcher = glob.compile_matcher();
                if matcher.is_match(path_str) {
                    return Ok(true);
                }
            }
        }
        Ok(false)
    }

    fn lfs_objects_dir(&self) -> PathBuf {
        self.repo().path().join("lfs").join("objects")
    }

    /// Validates that the OID is at least 4 characters for path splitting.
    fn lfs_validate_oid(&self, oid: &LfsOid) -> GitResult<()> {
        if oid.as_str().len() < 4 {
            return Err(GitError::Internal(format!(
                "LFS OID too short for path splitting: {}",
                oid
            )));
        }
        Ok(())
    }

    pub fn lfs_object_cached(&self, oid: &LfsOid) -> bool {
        if oid.as_str().len() < 4 {
            return false;
        }
        let (p1, rest) = oid.as_str().split_at(2);
        let (p2, _) = rest.split_at(2);
        self.lfs_objects_dir()
            .join(p1)
            .join(p2)
            .join(oid.as_str())
            .exists()
    }

    pub fn lfs_object_path(&self, oid: &LfsOid) -> GitResult<PathBuf> {
        self.lfs_validate_oid(oid)?;
        let (p1, rest) = oid.as_str().split_at(2);
        let (p2, _) = rest.split_at(2);
        Ok(self.lfs_objects_dir().join(p1).join(p2).join(oid.as_str()))
    }

    pub fn lfs_object_put(&self, oid: &LfsOid, content: &[u8]) -> GitResult<PathBuf> {
        use std::io::Write;

        let path = self.lfs_object_path(oid)?;
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).map_err(|e| GitError::IoError(e.to_string()))?;
        }
        let mut file = fs::File::create(&path).map_err(|e| GitError::IoError(e.to_string()))?;
        file.write_all(content)
            .map_err(|e| GitError::IoError(e.to_string()))?;
        Ok(path)
    }

    pub fn lfs_object_get(&self, oid: &LfsOid) -> GitResult<Vec<u8>> {
        let path = self.lfs_object_path(oid)?;
        if !path.exists() {
            return Err(GitError::LfsError(format!(
                "object {} not found in local cache",
                oid
            )));
        }
        fs::read(&path).map_err(|e| GitError::IoError(e.to_string()))
    }

    pub fn lfs_object_list(&self) -> GitResult<Vec<LfsOid>> {
        let base = self.lfs_objects_dir();
        if !base.exists() {
            return Ok(Vec::new());
        }
        let mut oids = Vec::new();
        self.lfs_object_list_impl(&base, &mut oids)?;
        Ok(oids)
    }

    fn lfs_object_list_impl(&self, dir: &Path, oids: &mut Vec<LfsOid>) -> GitResult<()> {
        for entry in fs::read_dir(dir).map_err(|e| GitError::IoError(e.to_string()))? {
            let entry = entry.map_err(|e| GitError::IoError(e.to_string()))?;
            let path = entry.path();
            if path.is_dir() {
                self.lfs_object_list_impl(&path, oids)?;
            } else if path.is_file() {
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    oids.push(LfsOid::new(name));
                }
            }
        }
        Ok(())
    }

    pub fn lfs_object_delete(&self, oid: &LfsOid) -> GitResult<bool> {
        let path = self.lfs_object_path(oid)?;
        if path.exists() {
            fs::remove_file(&path).map_err(|e| GitError::IoError(e.to_string()))?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    pub fn lfs_cache_size(&self) -> GitResult<u64> {
        let oids = self.lfs_object_list()?;
        let mut total = 0u64;
        for oid in oids {
            if let Ok(path) = self.lfs_object_path(&oid) {
                if let Ok(meta) = fs::metadata(&path) {
                    total += meta.len();
                }
            }
        }
        Ok(total)
    }

    pub fn lfs_config(&self) -> GitResult<LfsConfig> {
        let root = self.repo().workdir().unwrap_or_else(|| self.repo().path());
        let path = root.join(".lfsconfig");
        if !path.exists() {
            return Ok(LfsConfig::new());
        }
        let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?;
        let mut config = LfsConfig::new();
        for line in content.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            if let Some((k, v)) = line.split_once('=') {
                let k = k.trim();
                let v = v.trim();
                if k == "lfs.url" || k == "lfs.endpoint" {
                    config.endpoint = Some(v.to_string());
                } else if k == "lfs.accesskey" || k == "lfs.access_token" {
                    config.access_token = Some(v.to_string());
                }
            }
        }
        Ok(config)
    }

    pub fn lfs_config_set(&self, config: &LfsConfig) -> GitResult<()> {
        let root = self.repo().workdir().unwrap_or_else(|| self.repo().path());
        let path = root.join(".lfsconfig");
        let mut lines = Vec::new();
        if let Some(ref ep) = config.endpoint {
            lines.push(format!("lfs.url = {}", ep));
        }
        if let Some(ref tok) = config.access_token {
            lines.push(format!("lfs.access_token = {}", tok));
        }
        fs::write(&path, lines.join("\n") + "\n").map_err(|e| GitError::IoError(e.to_string()))?;
        Ok(())
    }
}
