pub(crate) mod artifact_transfer;
pub mod capabilities;
pub mod catalog;
pub mod delete;
pub use delete::DeleteResult;
pub mod gguf;
pub mod inventory;
pub mod local;
mod maintenance;
pub mod remote_catalog;
pub mod resolve;
pub use resolve::ResolvedModel;
#[cfg(test)]
mod delete_tests;
pub mod search;
pub mod topology;
mod usage;

use anyhow::{Context, Result};
use hf_hub::{HFClient, HFClientBuilder, HFClientSync};

pub use capabilities::{
    runtime_verified_model_capabilities, CapabilityLevel, ModelCapabilities,
    RuntimeMediaCapabilityEvidence,
};
pub use inventory::{scan_local_inventory_snapshot_with_progress, LocalModelInventorySnapshot};
pub use local::{
    find_mmproj_path, find_model_path, huggingface_hub_cache_dir, huggingface_identity_for_path,
    layered_package_layer_count_for_path, layered_package_total_bytes_for_path, mesh_llm_cache_dir,
    model_ref_for_path, scan_installed_models, scan_local_models,
};
pub use maintenance::{run_update, warn_about_updates_for_paths};
pub use resolve::{
    canonicalize_interest_model_ref, download_model_ref_with_progress_details,
    find_loaded_remote_catalog_model_exact, find_remote_catalog_model_exact,
    installed_model_capabilities, installed_model_display_name, installed_model_huggingface_ref,
    remote_catalog_model_draft_ref, remote_catalog_model_ref, resolve_model_spec,
    resolve_model_spec_with_progress, show_exact_model, show_model_variants_with_progress,
    ModelDetails, ShowVariantsProgress,
};
pub use search::{
    search_catalog_json_payload, search_catalog_models, search_huggingface,
    search_huggingface_json_payload, SearchArtifactFilter, SearchHit, SearchProgress, SearchSort,
};
pub use topology::{infer_local_model_topology, ModelMoeInfo, ModelTopology};
pub use usage::{
    execute_model_cleanup, load_model_usage_record_for_path, model_usage_cache_dir,
    plan_model_cleanup, track_managed_model_usage, track_model_usage, ModelCleanupPlan,
    ModelCleanupResult,
};

pub(crate) fn build_hf_api(_progress: bool) -> Result<HFClientSync> {
    let mut builder = HFClientBuilder::new().cache_dir(huggingface_hub_cache_dir());
    if let Ok(endpoint) = std::env::var("HF_ENDPOINT") {
        let endpoint = endpoint.trim();
        if !endpoint.is_empty() {
            builder = builder.endpoint(endpoint.to_string());
        }
    }
    if let Some(token) = hf_token_override() {
        builder = builder.token(token);
    }
    HFClientSync::from_api(builder.build().context("Build Hugging Face API client")?)
        .context("Build Hugging Face sync API client")
}

pub(crate) fn run_hf_sync<T, F>(operation: F) -> Result<T>
where
    T: Send + 'static,
    F: FnOnce() -> Result<T> + Send + 'static,
{
    if tokio::runtime::Handle::try_current().is_ok() {
        std::thread::spawn(operation).join().map_err(|panic| {
            if let Some(message) = panic.downcast_ref::<&str>() {
                anyhow::anyhow!("Hugging Face sync task panicked: {message}")
            } else if let Some(message) = panic.downcast_ref::<String>() {
                anyhow::anyhow!("Hugging Face sync task panicked: {message}")
            } else {
                anyhow::anyhow!("Hugging Face sync task panicked")
            }
        })?
    } else {
        operation()
    }
}

pub(crate) fn build_hf_tokio_api(_progress: bool) -> Result<HFClient> {
    let mut builder = HFClientBuilder::new().cache_dir(huggingface_hub_cache_dir());
    if let Ok(endpoint) = std::env::var("HF_ENDPOINT") {
        let endpoint = endpoint.trim();
        if !endpoint.is_empty() {
            builder = builder.endpoint(endpoint.to_string());
        }
    }
    if let Some(token) = hf_token_override() {
        builder = builder.token(token);
    }
    builder
        .build()
        .context("Build Hugging Face async API client")
}

pub(crate) fn hf_token_override() -> Option<String> {
    for key in ["HF_TOKEN", "HUGGING_FACE_HUB_TOKEN"] {
        if let Ok(token) = std::env::var(key) {
            let token = token.trim();
            if !token.is_empty() {
                return Some(token.to_string());
            }
        }
    }
    None
}

fn format_size_bytes(bytes: u64) -> String {
    if bytes >= 1_000_000_000 {
        format!("{:.1}GB", bytes as f64 / 1e9)
    } else {
        format!("{:.0}MB", bytes as f64 / 1e6)
    }
}

fn short_revision(revision: &str) -> String {
    if revision.len() <= 12 {
        revision.to_string()
    } else {
        revision[..12].to_string()
    }
}

#[cfg(test)]
mod tests {
    use crate::models::maintenance::cache_repo_id_from_dir;
    use crate::models::resolve::{parse_hf_resolve_url, parse_huggingface_ref};

    #[test]
    fn parse_hf_resolve_url_extracts_repo_revision_and_file() {
        let (repo, revision, file) = parse_hf_resolve_url(
            "https://huggingface.co/Qwen/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf",
        )
        .unwrap();
        assert_eq!(repo, "Qwen/Qwen3-8B-GGUF");
        assert_eq!(revision.as_deref(), Some("main"));
        assert_eq!(file, "Qwen3-8B-Q4_K_M.gguf");
    }

    #[test]
    fn cache_repo_id_from_dir_decodes_hf_cache_names() {
        assert_eq!(
            cache_repo_id_from_dir("models--Qwen--Qwen3-8B-GGUF"),
            Some("Qwen/Qwen3-8B-GGUF".to_string())
        );
    }

    #[test]
    fn parse_huggingface_ref_accepts_revision_shorthand() {
        let (repo, revision, file) =
            parse_huggingface_ref("Qwen/Qwen3-8B-GGUF@main/Qwen3-8B-Q4_K_M.gguf").unwrap();
        assert_eq!(repo, "Qwen/Qwen3-8B-GGUF");
        assert_eq!(revision.as_deref(), Some("main"));
        assert_eq!(file, "Qwen3-8B-Q4_K_M.gguf");
    }

    #[tokio::test]
    async fn run_hf_sync_leaves_tokio_runtime_context() {
        let saw_runtime = super::run_hf_sync(|| Ok(tokio::runtime::Handle::try_current().is_ok()))
            .expect("sync operation should run");
        assert!(!saw_runtime);
    }
}
