//! crates.io registry client.
//!
//! Provides access to crates.io via:
//! - Sparse index protocol (<https://index.crates.io>) for version lookups
//! - REST API (<https://crates.io/api/v1>) for search
//!
//! All HTTP requests are cached aggressively using ETag/Last-Modified headers.
//!
//! # Examples
//!
//! ```no_run
//! use deps_cargo::CratesIoRegistry;
//! use deps_core::HttpCache;
//! use std::sync::Arc;
//!
//! #[tokio::main]
//! async fn main() {
//!     let cache = Arc::new(HttpCache::new());
//!     let registry = CratesIoRegistry::new(cache);
//!
//!     let versions = registry.get_versions("serde").await.unwrap();
//!     println!("Latest serde: {}", versions[0].num);
//! }
//! ```

use crate::types::{CargoVersion, CrateInfo};
use deps_core::{DepsError, HttpCache, Result};
use semver::{Version, VersionReq};
use serde::Deserialize;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;

const SPARSE_INDEX_BASE: &str = "https://index.crates.io";
const SEARCH_API_BASE: &str = "https://crates.io/api/v1";

/// Base URL for crate pages on crates.io
pub const CRATES_IO_URL: &str = "https://crates.io/crates";

/// Returns the URL for a crate's page on crates.io.
pub fn crate_url(name: &str) -> String {
    format!("{CRATES_IO_URL}/{name}")
}

/// Client for interacting with crates.io registry.
///
/// Uses the sparse index protocol for fast version lookups and the REST API
/// for package search. All requests are cached via the provided HttpCache.
#[derive(Clone)]
pub struct CratesIoRegistry {
    cache: Arc<HttpCache>,
}

impl CratesIoRegistry {
    /// Creates a new registry client with the given HTTP cache.
    pub const fn new(cache: Arc<HttpCache>) -> Self {
        Self { cache }
    }

    /// Fetches all versions for a crate from the sparse index.
    ///
    /// Returns versions sorted newest-first. Includes yanked versions.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - HTTP request fails
    /// - Response body is invalid UTF-8
    /// - JSON parsing fails
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use deps_cargo::CratesIoRegistry;
    /// # use deps_core::HttpCache;
    /// # use std::sync::Arc;
    /// # #[tokio::main]
    /// # async fn main() {
    /// let cache = Arc::new(HttpCache::new());
    /// let registry = CratesIoRegistry::new(cache);
    ///
    /// let versions = registry.get_versions("serde").await.unwrap();
    /// assert!(!versions.is_empty());
    /// # }
    /// ```
    pub async fn get_versions(&self, name: &str) -> Result<Vec<CargoVersion>> {
        let path = sparse_index_path(name);
        // Pre-allocate: SPARSE_INDEX_BASE (25 chars) + "/" + path
        let mut url = String::with_capacity(SPARSE_INDEX_BASE.len() + 1 + path.len());
        url.push_str(SPARSE_INDEX_BASE);
        url.push('/');
        url.push_str(&path);

        let data = self.cache.get_cached(&url).await?;

        parse_index_json(&data, name)
    }

    /// Finds the latest version matching the given semver requirement.
    ///
    /// Only returns non-yanked versions.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Version requirement string is invalid semver
    /// - HTTP request fails
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use deps_cargo::CratesIoRegistry;
    /// # use deps_core::HttpCache;
    /// # use std::sync::Arc;
    /// # #[tokio::main]
    /// # async fn main() {
    /// let cache = Arc::new(HttpCache::new());
    /// let registry = CratesIoRegistry::new(cache);
    ///
    /// let latest = registry.get_latest_matching("serde", "^1.0").await.unwrap();
    /// assert!(latest.is_some());
    /// # }
    /// ```
    pub async fn get_latest_matching(
        &self,
        name: &str,
        req_str: &str,
    ) -> Result<Option<CargoVersion>> {
        let versions = self.get_versions(name).await?;

        let req = req_str
            .parse::<VersionReq>()
            .map_err(|e| DepsError::InvalidVersionReq(e.to_string()))?;

        Ok(versions.into_iter().find(|v| {
            let version = v.num.parse::<Version>().ok();
            version.is_some_and(|ver| req.matches(&ver) && !v.yanked)
        }))
    }

    /// Searches for crates by name/keywords.
    ///
    /// Returns up to `limit` results sorted by relevance.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - HTTP request fails
    /// - JSON parsing fails
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use deps_cargo::CratesIoRegistry;
    /// # use deps_core::HttpCache;
    /// # use std::sync::Arc;
    /// # #[tokio::main]
    /// # async fn main() {
    /// let cache = Arc::new(HttpCache::new());
    /// let registry = CratesIoRegistry::new(cache);
    ///
    /// let results = registry.search("serde", 10).await.unwrap();
    /// assert!(!results.is_empty());
    /// # }
    /// ```
    pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<CrateInfo>> {
        let url = format!(
            "{}/crates?q={}&per_page={}&sort=downloads",
            SEARCH_API_BASE,
            urlencoding::encode(query),
            limit
        );

        let data = self.cache.get_cached(&url).await?;
        parse_search_response(&data)
    }
}

/// Converts a crate name to its sparse index path.
///
/// Based on Cargo RFC 2789 specification:
/// - 1 char: "1/{name}"
/// - 2 chars: "2/{name}"
/// - 3 chars: "3/{first_char}/{name}"
/// - 4+ chars: "{first_2}/{next_2}/{name}"
fn sparse_index_path(name: &str) -> String {
    let name_lower = name.to_lowercase();
    let len = name_lower.len();

    match len {
        1 => {
            // "1/" + name = 2 + 1 = 3 chars
            let mut path = String::with_capacity(3);
            path.push_str("1/");
            path.push_str(&name_lower);
            path
        }
        2 => {
            // "2/" + name = 2 + 2 = 4 chars
            let mut path = String::with_capacity(4);
            path.push_str("2/");
            path.push_str(&name_lower);
            path
        }
        3 => {
            // "3/" + first_char + "/" + name = 2 + 1 + 1 + 3 = 7 chars
            let mut path = String::with_capacity(7);
            path.push_str("3/");
            path.push_str(&name_lower[0..1]);
            path.push('/');
            path.push_str(&name_lower);
            path
        }
        _ => {
            // first_2 + "/" + next_2 + "/" + name = 2 + 1 + 2 + 1 + len
            let mut path = String::with_capacity(6 + len);
            path.push_str(&name_lower[0..2]);
            path.push('/');
            path.push_str(&name_lower[2..4]);
            path.push('/');
            path.push_str(&name_lower);
            path
        }
    }
}

/// Entry in the sparse index (one line of newline-delimited JSON).
#[derive(Deserialize)]
struct IndexEntry {
    #[serde(rename = "vers")]
    version: String,
    #[serde(default)]
    yanked: bool,
    #[serde(default)]
    features: HashMap<String, Vec<String>>,
}

/// Parses newline-delimited JSON from sparse index.
fn parse_index_json(data: &[u8], _crate_name: &str) -> Result<Vec<CargoVersion>> {
    let content = std::str::from_utf8(data)
        .map_err(|e| DepsError::CacheError(format!("Invalid UTF-8: {e}")))?;

    // Parse versions once and cache the parsed Version for sorting
    let mut versions_with_parsed: Vec<(CargoVersion, Version)> = content
        .lines()
        .filter(|line| !line.trim().is_empty())
        .filter_map(|line| {
            let entry: IndexEntry = serde_json::from_str(line).ok()?;
            let parsed = entry.version.parse::<Version>().ok()?;
            Some((
                CargoVersion {
                    num: entry.version,
                    yanked: entry.yanked,
                    features: entry.features,
                },
                parsed,
            ))
        })
        .collect();

    // Sort using already-parsed versions (newest first)
    versions_with_parsed.sort_unstable_by(|a, b| b.1.cmp(&a.1));

    // Extract sorted versions
    Ok(versions_with_parsed.into_iter().map(|(v, _)| v).collect())
}

/// Response from crates.io search API.
#[derive(Deserialize)]
struct SearchResponse {
    crates: Vec<SearchCrate>,
}

/// Crate entry in search response.
#[derive(Deserialize)]
struct SearchCrate {
    name: String,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    repository: Option<String>,
    #[serde(default)]
    documentation: Option<String>,
    max_version: String,
}

/// Parses JSON response from crates.io search API.
fn parse_search_response(data: &[u8]) -> Result<Vec<CrateInfo>> {
    let response: SearchResponse = serde_json::from_slice(data)?;

    Ok(response
        .crates
        .into_iter()
        .map(|c| CrateInfo {
            name: c.name,
            description: c.description,
            repository: c.repository,
            documentation: c.documentation,
            max_version: c.max_version,
        })
        .collect())
}

impl deps_core::Registry for CratesIoRegistry {
    fn get_versions<'a>(
        &'a self,
        name: &'a str,
    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
        Box::pin(async move {
            let versions = self.get_versions(name).await?;
            Ok(versions
                .into_iter()
                .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
                .collect())
        })
    }

    fn get_latest_matching<'a>(
        &'a self,
        name: &'a str,
        req: &'a str,
    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
        Box::pin(async move {
            let version = self.get_latest_matching(name, req).await?;
            Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
        })
    }

    fn search<'a>(
        &'a self,
        query: &'a str,
        limit: usize,
    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
        Box::pin(async move {
            let results = self.search(query, limit).await?;
            Ok(results
                .into_iter()
                .map(|m| Box::new(m) as Box<dyn deps_core::Metadata>)
                .collect())
        })
    }

    fn package_url(&self, name: &str) -> String {
        crate_url(name)
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

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

    #[test]
    fn test_sparse_index_path() {
        assert_eq!(sparse_index_path("a"), "1/a");
        assert_eq!(sparse_index_path("ab"), "2/ab");
        assert_eq!(sparse_index_path("abc"), "3/a/abc");
        assert_eq!(sparse_index_path("serde"), "se/rd/serde");
        assert_eq!(sparse_index_path("tokio"), "to/ki/tokio");
    }

    #[test]
    fn test_sparse_index_path_uppercase() {
        assert_eq!(sparse_index_path("SERDE"), "se/rd/serde");
    }

    #[test]
    fn test_parse_index_json() {
        let json = r#"{"name":"serde","vers":"1.0.0","yanked":false,"features":{},"deps":[]}
{"name":"serde","vers":"1.0.1","yanked":false,"features":{"derive":["serde_derive"]},"deps":[]}"#;

        let versions = parse_index_json(json.as_bytes(), "serde").unwrap();
        assert_eq!(versions.len(), 2);
        assert_eq!(versions[0].num, "1.0.1");
        assert_eq!(versions[1].num, "1.0.0");
        assert!(!versions[0].yanked);
    }

    #[test]
    fn test_parse_index_json_with_yanked() {
        let json = r#"{"name":"test","vers":"0.1.0","yanked":true,"features":{},"deps":[]}
{"name":"test","vers":"0.2.0","yanked":false,"features":{},"deps":[]}"#;

        let versions = parse_index_json(json.as_bytes(), "test").unwrap();
        assert_eq!(versions.len(), 2);
        assert!(versions[1].yanked);
        assert!(!versions[0].yanked);
    }

    #[test]
    fn test_parse_search_response() {
        let json = r#"{
            "crates": [
                {
                    "name": "serde",
                    "description": "A serialization framework",
                    "repository": "https://github.com/serde-rs/serde",
                    "documentation": "https://docs.rs/serde",
                    "max_version": "1.0.214"
                }
            ]
        }"#;

        let results = parse_search_response(json.as_bytes()).unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].name, "serde");
        assert_eq!(results[0].max_version, "1.0.214");
    }

    #[tokio::test]
    #[ignore]
    async fn test_fetch_real_serde_versions() {
        let cache = Arc::new(HttpCache::new());
        let registry = CratesIoRegistry::new(cache);
        let versions = registry.get_versions("serde").await.unwrap();

        assert!(!versions.is_empty());
        assert!(versions.iter().any(|v| v.num.starts_with("1.")));
    }

    #[tokio::test]
    #[ignore]
    async fn test_search_real() {
        let cache = Arc::new(HttpCache::new());
        let registry = CratesIoRegistry::new(cache);
        let results = registry.search("serde", 5).await.unwrap();

        assert!(!results.is_empty());
        assert!(results.iter().any(|r| r.name == "serde"));
    }

    #[tokio::test]
    #[ignore]
    async fn test_get_latest_matching_real() {
        let cache = Arc::new(HttpCache::new());
        let registry = CratesIoRegistry::new(cache);
        let latest = registry.get_latest_matching("serde", "^1.0").await.unwrap();

        assert!(latest.is_some());
        let version = latest.unwrap();
        assert!(version.num.starts_with("1."));
        assert!(!version.yanked);
    }

    #[test]
    fn test_parse_index_json_empty() {
        let json = "";
        let versions = parse_index_json(json.as_bytes(), "test").unwrap();
        assert_eq!(versions.len(), 0);
    }

    #[test]
    fn test_parse_index_json_blank_lines() {
        let json = "\n\n\n";
        let versions = parse_index_json(json.as_bytes(), "test").unwrap();
        assert_eq!(versions.len(), 0);
    }

    #[test]
    fn test_parse_index_json_invalid_version() {
        let json = r#"{"name":"test","vers":"invalid","yanked":false,"features":{},"deps":[]}"#;
        let versions = parse_index_json(json.as_bytes(), "test").unwrap();
        assert_eq!(versions.len(), 0);
    }

    #[test]
    fn test_parse_index_json_mixed_valid_invalid() {
        let json = r#"{"name":"test","vers":"1.0.0","yanked":false,"features":{},"deps":[]}
{"name":"test","vers":"invalid","yanked":false,"features":{},"deps":[]}
{"name":"test","vers":"2.0.0","yanked":false,"features":{},"deps":[]}"#;

        let versions = parse_index_json(json.as_bytes(), "test").unwrap();
        assert_eq!(versions.len(), 2);
        assert_eq!(versions[0].num, "2.0.0");
        assert_eq!(versions[1].num, "1.0.0");
    }

    #[test]
    fn test_parse_index_json_with_features() {
        let json = r#"{"name":"test","vers":"1.0.0","yanked":false,"features":{"default":["std"],"std":[]},"deps":[]}"#;

        let versions = parse_index_json(json.as_bytes(), "test").unwrap();
        assert_eq!(versions.len(), 1);
        assert_eq!(versions[0].features.len(), 2);
        assert!(versions[0].features.contains_key("default"));
        assert!(versions[0].features.contains_key("std"));
    }

    #[test]
    fn test_parse_search_response_empty() {
        let json = r#"{"crates": []}"#;
        let results = parse_search_response(json.as_bytes()).unwrap();
        assert_eq!(results.len(), 0);
    }

    #[test]
    fn test_parse_search_response_missing_optional_fields() {
        let json = r#"{
            "crates": [
                {
                    "name": "minimal",
                    "max_version": "1.0.0"
                }
            ]
        }"#;

        let results = parse_search_response(json.as_bytes()).unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].name, "minimal");
        assert_eq!(results[0].description, None);
        assert_eq!(results[0].repository, None);
    }

    #[test]
    fn test_sparse_index_path_single_char() {
        assert_eq!(sparse_index_path("x"), "1/x");
        assert_eq!(sparse_index_path("z"), "1/z");
    }

    #[test]
    fn test_sparse_index_path_two_chars() {
        assert_eq!(sparse_index_path("xy"), "2/xy");
        assert_eq!(sparse_index_path("ab"), "2/ab");
    }

    #[test]
    fn test_sparse_index_path_three_chars() {
        assert_eq!(sparse_index_path("xyz"), "3/x/xyz");
        assert_eq!(sparse_index_path("foo"), "3/f/foo");
    }

    #[test]
    fn test_sparse_index_path_long_name() {
        assert_eq!(
            sparse_index_path("very-long-crate-name"),
            "ve/ry/very-long-crate-name"
        );
    }

    #[test]
    fn test_sparse_index_path_numbers() {
        assert_eq!(sparse_index_path("1234"), "12/34/1234");
    }

    #[test]
    fn test_sparse_index_path_mixed_case() {
        assert_eq!(sparse_index_path("MyPackage"), "my/pa/mypackage");
        assert_eq!(sparse_index_path("UPPERCASE"), "up/pe/uppercase");
    }

    #[test]
    fn test_crate_url() {
        assert_eq!(crate_url("serde"), "https://crates.io/crates/serde");
        assert_eq!(crate_url("tokio"), "https://crates.io/crates/tokio");
    }

    #[test]
    fn test_crate_url_with_hyphens() {
        assert_eq!(
            crate_url("serde-json"),
            "https://crates.io/crates/serde-json"
        );
    }

    #[tokio::test]
    async fn test_registry_creation() {
        let cache = Arc::new(HttpCache::new());
        let _registry = CratesIoRegistry::new(cache);
    }

    #[tokio::test]
    async fn test_registry_clone() {
        let cache = Arc::new(HttpCache::new());
        let registry = CratesIoRegistry::new(cache);
        let _cloned = registry;
    }
}
