use super::CrateFetcher;
use crate::models::ScanCrateError;
use anyhow::Result;
use async_trait::async_trait;
use crates_io_api::SyncClient;
use dirs;
use flate2::read::GzDecoder;
use std::fs::{self, File};
use std::path::PathBuf;
use std::process::Command;
use tar::Archive;
use tempfile::TempDir;
use walkdir::WalkDir;

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

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

    /// Checks if a crate exists in Cargo's registry cache
    fn check_cargo_registry_cache(name: &str, version: &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"));

        // Check registry/src directory
        let registry_src = cargo_home.join("registry").join("src");
        if !registry_src.exists() {
            return None;
        }

        // Look for the crate in any registry source directory
        // Typical paths are like: ~/.cargo/registry/src/index.crates.io-{hash}/{name}-{version}
        for entry in std::fs::read_dir(&registry_src).ok()? {
            let entry = entry.ok()?;
            if entry.file_type().ok()?.is_dir() {
                let crate_dir = entry.path().join(format!("{name}-{version}"));
                if crate_dir.exists() && crate_dir.join("Cargo.toml").exists() {
                    log::info!(
                        "Found crate {name}-{version} in Cargo registry cache at {crate_dir:?}"
                    );
                    return Some(crate_dir);
                }
            }
        }

        None
    }

    // Synchronous function to resolve a version range to an exact version using crates.io API
    fn resolve_version_range_sync(
        name: &str,
        version_req_str: &str,
    ) -> Result<String, ScanCrateError> {
        log::info!(
            "[VER_DETECT] Resolving version range '{version_req_str}' for crate '{name}' using crates.io API (sync)."
        );

        let client = SyncClient::new(
            "scan_crate/0.1.0 (dev@example.com)",
            std::time::Duration::from_millis(1000),
        )
        .map_err(|e| {
            ScanCrateError::Other(anyhow::anyhow!("crates_io_api client init failed: {}", e))
        })?;

        let crate_info = match client.get_crate(name) {
            Ok(info) => info,
            Err(e) => {
                return Err(ScanCrateError::Other(anyhow::anyhow!(
                    "crates_io_api get_crate failed for {}: {}",
                    name,
                    e
                )))
            }
        };

        if crate_info.versions.is_empty() {
            return Err(ScanCrateError::Other(anyhow::anyhow!(
                "No versions found for crate '{}'",
                name
            )));
        }

        let parsed_semver_req = semver::VersionReq::parse(version_req_str).map_err(|e| {
            ScanCrateError::Other(anyhow::anyhow!(
                "Failed to re-parse semver requirement '{}': {}",
                version_req_str,
                e
            ))
        })?;

        let max_satisfying_version = crate_info
            .versions
            .iter()
            .filter_map(|v| semver::Version::parse(&v.num).ok())
            .filter(|v| parsed_semver_req.matches(v))
            .max();

        if let Some(version) = max_satisfying_version {
            let resolved_version = version.to_string();
            log::info!(
                "[VER_DETECT] Resolved '{version_req_str}' for crate '{name}' to exact version: {resolved_version} using crates.io API (sync)."
            );
            Ok(resolved_version)
        } else {
            Err(ScanCrateError::Other(anyhow::anyhow!(
                "No version satisfying requirement '{}' found for crate '{}'",
                version_req_str,
                name
            )))
        }
    }

    // Synchronous function to get the latest version of a crate using crates.io API
    fn get_latest_version_sync(name: &str) -> Result<String, ScanCrateError> {
        log::info!(
            "[VER_DETECT] Getting latest version for crate '{name}' using crates.io API (sync)."
        );

        let client = SyncClient::new(
            "scan_crate/0.1.0 (dev@example.com)",
            std::time::Duration::from_millis(1000),
        )
        .map_err(|e| {
            ScanCrateError::Other(anyhow::anyhow!(
                "crates_io_api client init failed for latest: {}",
                e
            ))
        })?;

        let crate_info = match client.get_crate(name) {
            Ok(info) => info,
            Err(e) => {
                return Err(ScanCrateError::Other(anyhow::anyhow!(
                    "crates_io_api get_crate failed for {} (latest): {}",
                    name,
                    e
                )))
            }
        };

        if let Some(latest_version_info) = crate_info
            .crate_data
            .max_stable_version
            .as_ref()
            .or(Some(&crate_info.crate_data.max_version))
        {
            match semver::Version::parse(latest_version_info) {
                Ok(v) => {
                    let latest_version_str = v.to_string();
                    log::info!(
                        "[VER_DETECT] Resolved LATEST for crate '{name}' to version: {latest_version_str} using crates.io API (sync)."
                    );
                    Ok(latest_version_str)
                }
                Err(e) => Err(ScanCrateError::Other(anyhow::anyhow!(
                    "Failed to parse max_version '{}' from crates.io API for crate '{}': {}",
                    latest_version_info,
                    name,
                    e
                ))),
            }
        } else {
            Err(ScanCrateError::Other(anyhow::anyhow!(
                "Could not determine latest version for crate '{}' from crates.io API.",
                name
            )))
        }
    }
}

#[async_trait]
impl CrateFetcher for CratesIoFetcher {
    async fn fetch_crate_src(
        &self,
        name: &str,
        version_req: Option<&str>,
    ) -> Result<PathBuf, ScanCrateError> {
        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("crates");
        fs::create_dir_all(&base_cache_path).map_err(|e| {
            ScanCrateError::Other(anyhow::anyhow!(
                "Failed to create base cache directory {:?}: {}",
                base_cache_path,
                e
            ))
        })?;

        // TODO: Make the path to cargo-download configurable or discover it dynamically.
        let cargo_download_bin = PathBuf::from("/Users/daniel/.cargo/bin/cargo-download");

        let actual_version_str: String;

        if let Some(v_req_original) = version_req {
            let v_req_trimmed = v_req_original.trim();

            log::debug!(
                "[VER_DETECT] Original v_req: '{v_req_original}', Trimmed v_req: '{v_req_trimmed}'"
            );

            if v_req_trimmed.is_empty() {
                log::debug!("[VER_DETECT] Trimmed v_req is empty after trim.");
                return Err(ScanCrateError::InvalidCrateSpec {
                    spec: format!("{name}@{v_req_original}"),
                    source: anyhow::anyhow!("Version requirement part is empty or all whitespace"),
                });
            }

            let parsed_semver_req = match semver::VersionReq::parse(v_req_trimmed) {
                Ok(req) => {
                    log::debug!(
                        "[VER_DETECT] Successfully parsed '{}' with semver. Comparators: {:?}",
                        v_req_trimmed,
                        req.comparators
                    );
                    req
                }
                Err(e) => {
                    log::debug!(
                        "[VER_DETECT] Failed to parse '{v_req_trimmed}' with semver: {e}"
                    );
                    return Err(ScanCrateError::InvalidCrateSpec {
                        spec: format!("{name}@{v_req_trimmed}"),
                        source: anyhow::anyhow!(
                            "Invalid semver requirement '{}': {}",
                            v_req_trimmed,
                            e
                        ),
                    });
                }
            };

            let is_semver_exact_op = parsed_semver_req.comparators.len() == 1
                && parsed_semver_req.comparators[0].op == semver::Op::Exact;
            let starts_with_equals = v_req_trimmed.starts_with('=');
            let v_req_for_simple_check = v_req_trimmed.trim_start_matches('=');
            let is_simple_numeric_dot = v_req_for_simple_check
                .chars()
                .all(|c| c.is_ascii_digit() || c == '.');
            let non_empty_after_equals_strip = !v_req_trimmed.trim_start_matches('=').is_empty();
            let contains_no_semver_range_chars =
                !v_req_trimmed.contains(['^', '~', '*', '<', '>', ',']);

            log::debug!(
                "[VER_DETECT] Conditions for exact: is_exact_op: {is_semver_exact_op}, starts_eq: {starts_with_equals}, simple_num_dot (on '{v_req_for_simple_check}'): {is_simple_numeric_dot}, non_empty_strip: {non_empty_after_equals_strip}, no_range_chars: {contains_no_semver_range_chars}"
            );

            let is_determined_exact = (is_semver_exact_op && non_empty_after_equals_strip)
                || (is_simple_numeric_dot
                    && non_empty_after_equals_strip
                    && contains_no_semver_range_chars);

            if is_determined_exact {
                let version_to_use = v_req_trimmed.trim_start_matches('=').to_string();
                log::info!(
                    "[VER_DETECT] '{v_req_trimmed}' determined as EXACT. Using: {version_to_use}"
                );
                actual_version_str = version_to_use;
            } else {
                log::info!(
                    "[VER_DETECT] Version requirement '{v_req_trimmed}' for crate '{name}' is a RANGE. Using separate sync function."
                );

                // Use the sync function via spawn_blocking
                let name_clone = name.to_string();
                let v_req_trimmed_clone = v_req_trimmed.to_string();

                actual_version_str = tokio::task::spawn_blocking(move || {
                    CratesIoFetcher::resolve_version_range_sync(
                        &name_clone,
                        &v_req_trimmed_clone,
                    )
                })
                .await
                .map_err(|e| {
                    ScanCrateError::Other(anyhow::anyhow!(
                        "Task for resolving crate version failed: {}",
                        e
                    ))
                })??;

                log::info!(
                    "[VER_DETECT] Back in async context with resolved version: {actual_version_str}"
                );
            }
        } else {
            log::info!(
                "[VER_DETECT] No version_req. Using separate sync function to get latest version for '{name}'."
            );

            let name_clone = name.to_string();

            actual_version_str = tokio::task::spawn_blocking(move || {
                CratesIoFetcher::get_latest_version_sync(&name_clone)
            })
            .await
            .map_err(|e| {
                ScanCrateError::Other(anyhow::anyhow!(
                    "Task for resolving latest crate version failed: {}",
                    e
                ))
            })??;

            log::info!(
                "[VER_DETECT] Back in async context with latest version: {actual_version_str}"
            );
        }

        // First, check if the crate is already in Cargo's registry cache
        if let Some(cargo_cache_path) = Self::check_cargo_registry_cache(name, &actual_version_str) {
            log::info!(
                "Using crate {name}-{actual_version_str} from Cargo registry cache"
            );
            return Ok(cargo_cache_path);
        }

        let crate_version_cache_path = base_cache_path.join(name).join(&actual_version_str);

        if crate_version_cache_path.exists() {
            let entries = fs::read_dir(&crate_version_cache_path).map_err(|e| {
                ScanCrateError::Other(anyhow::anyhow!(
                    "Failed to read unpacked cache directory {:?}: {}",
                    crate_version_cache_path,
                    e
                ))
            })?;

            let mut found_src_root: Option<PathBuf> = None;
            for entry_result in entries {
                let entry = entry_result.map_err(|e| {
                    ScanCrateError::Other(anyhow::anyhow!(
                        "Error reading entry in unpacked cache dir {:?}: {}",
                        crate_version_cache_path,
                        e
                    ))
                })?;

                if entry.file_type().is_ok_and(|ft| ft.is_dir()) {
                    let path = entry.path();
                    if path.is_dir()
                        && path
                            .file_name()
                            .is_some_and(|os_name| os_name.to_string_lossy().starts_with(name))
                        && path.join("Cargo.toml").exists()
                    {
                        if found_src_root.is_some() {
                            return Err(ScanCrateError::Other(anyhow::anyhow!(
                                "Multiple potential src directories found in cache {:?}",
                                crate_version_cache_path
                            )));
                        }
                        found_src_root = Some(path);
                    }
                }
            }

            match found_src_root {
                Some(src_path) => Ok(src_path),
                None => Err(ScanCrateError::Other(anyhow::anyhow!("No source directory found after unpacking to cache {:?}. Expected subdir like '{}-{}'", crate_version_cache_path, name, actual_version_str))),
            }
        } else {
            log::info!(
                "Cache miss or invalid for {name} version {actual_version_str}. Downloading..."
            );

            let temp_download_dir = TempDir::new().map_err(|e| {
                ScanCrateError::Other(anyhow::anyhow!(
                    "Failed to create temp download directory: {}",
                    e
                ))
            })?;
            let crate_archive_path = temp_download_dir
                .path()
                .join(format!("{name}-McpCache-{actual_version_str}.crate"));

            let mut download_cmd_main = Command::new(cargo_download_bin.clone());
            let main_crate_spec = format!("{name}=={actual_version_str}");
            download_cmd_main.arg("-vv");
            download_cmd_main.arg(main_crate_spec.clone());
            download_cmd_main.arg("-o").arg(&crate_archive_path);
            log::info!(
                "Executing cargo-download (for caching): cargo-download -vv {main_crate_spec} -o {crate_archive_path:?}"
            );

            let output_main = download_cmd_main.output().map_err(|e| {
                ScanCrateError::FetcherError(anyhow::anyhow!(
                    "Failed to execute cargo-download for '{}': {}",
                    main_crate_spec,
                    e
                ))
            })?;

            if !output_main.status.success() {
                let stderr = String::from_utf8_lossy(&output_main.stderr);
                let stdout = String::from_utf8_lossy(&output_main.stdout);
                log::error!(
                    "cargo-download for caching for '{}' failed. Status: {}. Stderr: {}. Stdout: {}",
                    main_crate_spec,
                    output_main.status,
                    stderr,
                    stdout
                );
                return Err(ScanCrateError::CrateSourceNotFound {
                    name: name.to_string(),
                    version_spec: actual_version_str.clone(),
                    source: anyhow::anyhow!(
                        "cargo-download for caching for '{}' failed with status {}: Stderr: {}. Stdout: {}",
                        main_crate_spec, output_main.status, stderr, stdout
                    ),
                });
            }
            log::info!(
                "cargo-download for caching for '{main_crate_spec}' SUCCEEDED! Crate at {crate_archive_path:?}"
            );

            fs::create_dir_all(&crate_version_cache_path).map_err(|e| {
                ScanCrateError::Other(anyhow::anyhow!(
                    "Failed to create target cache directory {:?}: {}",
                    crate_version_cache_path,
                    e
                ))
            })?;

            let file_to_extract = &crate_archive_path;
            log::info!("Attempting to extract from: {file_to_extract:?}");

            let file = File::open(file_to_extract).map_err(|e| {
                ScanCrateError::FetcherError(anyhow::anyhow!(
                    "Failed to open downloaded .crate file {:?} (from main download): {}",
                    file_to_extract,
                    e
                ))
            })?;
            let gz = GzDecoder::new(file);
            let mut archive = Archive::new(gz);

            archive.unpack(&crate_version_cache_path).map_err(|e| {
                ScanCrateError::FetcherError(anyhow::anyhow!(
                    "Failed to unpack .crate to cache dir {:?}: {}",
                    crate_version_cache_path,
                    e
                ))
            })?;

            let mut found_src_root: Option<PathBuf> = None;
            for entry_result in WalkDir::new(&crate_version_cache_path) {
                let entry = match entry_result {
                    Ok(e) => e,
                    Err(err) => {
                        log::warn!(
                            "Error walking directory {crate_version_cache_path:?}: {err}"
                        );
                        continue;
                    }
                };
                if entry.depth() == 1 {
                    let path = entry.path();
                    if entry.file_type().is_dir()
                        && path.is_dir()
                        && path
                            .file_name()
                            .is_some_and(|os_name| os_name.to_string_lossy().starts_with(name))
                        && path.join("Cargo.toml").exists()
                    {
                        if found_src_root.is_some() {
                            return Err(ScanCrateError::Other(anyhow::anyhow!(
                                "Multiple potential src directories found in cache {:?}",
                                crate_version_cache_path
                            )));
                        }
                        found_src_root = Some(path.to_path_buf());
                    }
                }
            }

            match found_src_root {
                Some(src_path) => Ok(src_path),
                None => Err(ScanCrateError::Other(anyhow::anyhow!("No source directory found after unpacking to cache {:?}. Expected subdir like '{}-{}'", crate_version_cache_path, name, actual_version_str))),
            }
        }
    }
}