use super::CrateFetcher;
use crate::models::ScanCrateError;
use async_trait::async_trait;
use dirs;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

#[derive(Debug, Clone, Default)]
pub struct GitFetcher;

impl GitFetcher {
    #[must_use]
    pub fn new() -> Self {
        Self
    }

    /// Checks if a git repository is already in Cargo's git checkouts cache
    fn check_cargo_git_cache(url: &str, _git_ref: Option<&str>) -> Option<PathBuf> {
        let home_dir = dirs::home_dir()?;
        let cargo_home = std::env::var("CARGO_HOME")
            .ok()
            .map(PathBuf::from)
            .unwrap_or_else(|| home_dir.join(".cargo"));

        // Cargo stores git repos in git/checkouts
        let git_checkouts = cargo_home.join("git").join("checkouts");
        if !git_checkouts.exists() {
            return None;
        }

        // Create a hash-like identifier from the URL (simplified version)
        // Cargo uses a more complex hashing, but we'll do a simple search
        let url_parts: Vec<&str> = url.split('/').collect();
        let repo_name = url_parts.last()?.trim_end_matches(".git");
        
        // Look for directories that might contain this repo
        for entry in std::fs::read_dir(&git_checkouts).ok()? {
            let entry = entry.ok()?;
            if entry.file_type().ok()?.is_dir() {
                let dir_name = entry.file_name();
                let dir_name_str = dir_name.to_string_lossy();
                
                // Check if this looks like it could be our repo
                if dir_name_str.contains(repo_name) {
                    // Look inside for checkout directories
                    for checkout_entry in std::fs::read_dir(entry.path()).ok()? {
                        let checkout_entry = checkout_entry.ok()?;
                        if checkout_entry.file_type().ok()?.is_dir() {
                            let checkout_path = checkout_entry.path();
                            if checkout_path.join("Cargo.toml").exists() {
                                log::info!(
                                    "Found git repo {url} in Cargo git cache at {checkout_path:?}"
                                );
                                // TODO: Verify this is the correct ref/commit
                                return Some(checkout_path);
                            }
                        }
                    }
                }
            }
        }
        
        None
    }

    /// Parses a git URL specification into components
    /// Format: git+https://github.com/user/repo.git[@branch|@tag|@commit]
    fn parse_git_spec(spec: &str) -> Result<(String, Option<String>), ScanCrateError> {
        let spec = spec.trim();
        
        // Remove git+ prefix if present
        let spec = spec.strip_prefix("git+").unwrap_or(spec);
        
        // Find the last @ that's not part of the URL (e.g., not in user@host)
        let parts: Vec<&str> = spec.rsplitn(2, '@').collect();
        
        let (url, git_ref) = if parts.len() == 2 && !parts[0].contains("://") {
            // Found a ref specification
            (parts[1].to_string(), Some(parts[0].to_string()))
        } else {
            // No ref specification or @ is part of the URL
            (spec.to_string(), None)
        };
        
        // Validate it looks like a git URL
        if !url.contains("://") && !url.contains("git@") {
            return Err(ScanCrateError::InvalidCrateSpec {
                spec: spec.to_string(),
                source: anyhow::anyhow!("Invalid git URL format"),
            });
        }
        
        Ok((url, git_ref))
    }

    /// Creates a cache key from the git URL and ref
    fn cache_key(url: &str, git_ref: Option<&str>) -> String {
        let sanitized_url = url
            .replace("://", "_")
            .replace(['/', '\\', ':', '.'], "_");
        
        match git_ref {
            Some(ref_str) => format!("{sanitized_url}_{ref_str}"),
            None => sanitized_url,
        }
    }
}

#[async_trait]
impl CrateFetcher for GitFetcher {
    async fn fetch_crate_src(
        &self,
        name: &str,
        version_req: Option<&str>,
    ) -> Result<PathBuf, ScanCrateError> {
        // For git fetcher, the "name" is actually the git URL
        // and version_req might be a branch/tag/commit
        let (git_url, git_ref) = if let Some(version) = version_req {
            // If version is provided, treat it as ref
            (name.to_string(), Some(version.to_string()))
        } else if name.starts_with("http") || name.starts_with("git") || name.contains("github.com") {
            // Parse git URL from name if it looks like a URL
            Self::parse_git_spec(name)?
        } else {
            return Err(ScanCrateError::InvalidCrateSpec {
                spec: name.to_string(),
                source: anyhow::anyhow!("Not a valid git URL"),
            });
        };

        // First, check if the repo is already in Cargo's git cache
        if let Some(cargo_cache_path) = Self::check_cargo_git_cache(&git_url, git_ref.as_deref()) {
            log::info!(
                "Using git repository from Cargo git cache: {git_url}"
            );
            return Ok(cargo_cache_path);
        }

        let home_dir = dirs::home_dir().ok_or_else(|| {
            ScanCrateError::Other(anyhow::anyhow!("Could not determine home directory"))
        })?;
        
        let base_cache_path = home_dir.join(".scan_crate_cache").join("git");
        fs::create_dir_all(&base_cache_path).map_err(|e| {
            ScanCrateError::Other(anyhow::anyhow!(
                "Failed to create git cache directory {:?}: {}",
                base_cache_path,
                e
            ))
        })?;

        let cache_key = Self::cache_key(&git_url, git_ref.as_deref());
        let cache_path = base_cache_path.join(&cache_key);

        // Check if already cached
        if cache_path.exists() && cache_path.join("Cargo.toml").exists() {
            log::info!("Using cached git repository at {cache_path:?}");
            
            // If we have a specific ref and it's a commit hash, we're good
            // Otherwise, we should update to get latest changes
            if let Some(ref git_ref_str) = git_ref {
                if git_ref_str.len() == 40 && git_ref_str.chars().all(|c| c.is_ascii_hexdigit()) {
                    // Looks like a commit hash, no need to update
                    return Ok(cache_path);
                }
            }
            
            // Update the repository
            log::info!("Updating cached repository at {cache_path:?}");
            let mut fetch_cmd = Command::new("git");
            fetch_cmd.current_dir(&cache_path);
            fetch_cmd.args(["fetch", "--all"]);
            
            let output = fetch_cmd.output().map_err(|e| {
                ScanCrateError::FetcherError(anyhow::anyhow!(
                    "Failed to fetch updates for git repository: {}",
                    e
                ))
            })?;
            
            if !output.status.success() {
                log::warn!(
                    "Failed to update git repository: {}",
                    String::from_utf8_lossy(&output.stderr)
                );
            }
            
            // Checkout the requested ref
            if let Some(ref git_ref_str) = git_ref {
                let mut checkout_cmd = Command::new("git");
                checkout_cmd.current_dir(&cache_path);
                checkout_cmd.args(["checkout", git_ref_str]);
                
                let output = checkout_cmd.output().map_err(|e| {
                    ScanCrateError::FetcherError(anyhow::anyhow!(
                        "Failed to checkout git ref '{}': {}",
                        git_ref_str,
                        e
                    ))
                })?;
                
                if !output.status.success() {
                    return Err(ScanCrateError::FetcherError(anyhow::anyhow!(
                        "Failed to checkout git ref '{}': {}",
                        git_ref_str,
                        String::from_utf8_lossy(&output.stderr)
                    )));
                }
            }
            
            return Ok(cache_path);
        }

        // Clone the repository
        log::info!("Cloning git repository {} to {:?}", git_url, cache_path);
        
        let mut clone_cmd = Command::new("git");
        clone_cmd.arg("clone");
        
        // Add branch/tag specification if provided
        if let Some(ref git_ref_str) = git_ref {
            // Try to determine if it's a branch or tag
            if !git_ref_str.contains('/') && git_ref_str.len() != 40 {
                // Likely a branch or tag name, not a commit
                clone_cmd.args(["--branch", git_ref_str]);
            }
        }
        
        clone_cmd.arg(&git_url);
        clone_cmd.arg(&cache_path);
        
        let output = clone_cmd.output().map_err(|e| {
            ScanCrateError::FetcherError(anyhow::anyhow!(
                "Failed to execute git clone: {}",
                e
            ))
        })?;
        
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(ScanCrateError::CrateSourceNotFound {
                name: git_url.clone(),
                version_spec: git_ref.clone().unwrap_or_else(|| "latest".to_string()),
                source: anyhow::anyhow!(
                    "git clone failed: {}",
                    stderr
                ),
            });
        }
        
        // If we have a specific commit hash, checkout to it
        if let Some(ref git_ref_str) = git_ref {
            if git_ref_str.len() == 40 && git_ref_str.chars().all(|c| c.is_ascii_hexdigit()) {
                let mut checkout_cmd = Command::new("git");
                checkout_cmd.current_dir(&cache_path);
                checkout_cmd.args(["checkout", git_ref_str]);
                
                let output = checkout_cmd.output().map_err(|e| {
                    ScanCrateError::FetcherError(anyhow::anyhow!(
                        "Failed to checkout commit '{}': {}",
                        git_ref_str,
                        e
                    ))
                })?;
                
                if !output.status.success() {
                    return Err(ScanCrateError::FetcherError(anyhow::anyhow!(
                        "Failed to checkout commit '{}': {}",
                        git_ref_str,
                        String::from_utf8_lossy(&output.stderr)
                    )));
                }
            }
        }
        
        // Verify Cargo.toml exists
        if !cache_path.join("Cargo.toml").exists() {
            return Err(ScanCrateError::Other(anyhow::anyhow!(
                "Repository does not contain a Cargo.toml file"
            )));
        }
        
        Ok(cache_path)
    }
}