//! Model management service for downloading and caching AI models from HuggingFace Hub
//!
//! This service provides a unified interface for:
//! - Downloading models from HuggingFace Hub using the model catalog
//! - Caching models to the project's models/ directory
//! - Listing available and installed models
//! - Managing model lifecycle (pull, remove, info)
//!
//! # Design
//!
//! - Models are defined in `model_catalog.toml` embedded in the CLI binary
//! - Models are downloaded from HuggingFace Hub on first use (lazy loading)
//! - Models are cached to `<project_root>/models/<name>.onnx`
//! - HF cache is also used (`~/.cache/huggingface/hub/`) for offline support
//!
//! # Usage
//!
//! ```no_run
//! use mecha10_cli::services::ModelService;
//!
//! let service = ModelService::new()?;
//!
//! // List catalog models
//! let catalog = service.list_catalog()?;
//!
//! // Download a model
//! service.pull("yolov8n", None)?;
//!
//! // List installed models
//! let installed = service.list_installed()?;
//! ```

use anyhow::{Context, Result};
use hf_hub::api::tokio::Api;
use indicatif::{ProgressBar, ProgressStyle};
use mecha10_core::model::{CustomLabelsConfig, ModelConfig, PreprocessingConfig};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;

/// Model catalog entry from model_catalog.toml
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCatalogEntry {
    /// Model identifier (e.g., "yolov8n-face", "mobilenet-v2")
    pub name: String,
    /// Human-readable description
    pub description: String,
    /// Task type (e.g., "object-detection", "image-classification")
    pub task: String,
    /// HuggingFace repository (e.g., "deepghs/yolo-face")
    pub repo: String,
    /// Filename within the repo (e.g., "yolov8n-face/model.onnx")
    pub filename: String,

    /// Optional preprocessing preset hint (e.g., "imagenet", "yolo", "coco")
    #[serde(default)]
    pub preprocessing_preset: Option<String>,

    /// Optional small class lists (e.g., ["face"] for face detection)
    #[serde(default)]
    pub classes: Vec<String>,

    /// Optional automatic quantization configuration
    #[serde(default)]
    pub quantize: Option<QuantizeConfig>,
}

/// Quantization configuration for automatic INT8 conversion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuantizeConfig {
    /// Enable automatic quantization after download
    pub enabled: bool,
    /// Quantization method (currently only "dynamic_int8" supported)
    pub method: String,
}

/// Model catalog loaded from TOML
#[derive(Debug, Deserialize)]
struct ModelCatalog {
    models: Vec<ModelCatalogEntry>,
}

/// Preprocessing presets for common model families
#[derive(Debug, Clone, Copy)]
pub enum PreprocessingPreset {
    /// ImageNet normalization (mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ImageNet,
    /// YOLO normalization (mean=[0.0, 0.0, 0.0], std=[255.0, 255.0, 255.0] -> [0, 1])
    Yolo,
    /// COCO normalization (same as ImageNet)
    Coco,
    /// No normalization ([0, 255] range)
    Zero255,
}

impl PreprocessingPreset {
    /// Parse preset from string name
    pub fn from_name(name: &str) -> Result<Self> {
        match name.to_lowercase().as_str() {
            "imagenet" => Ok(Self::ImageNet),
            "yolo" => Ok(Self::Yolo),
            "coco" => Ok(Self::Coco),
            "zero255" | "0-255" => Ok(Self::Zero255),
            _ => anyhow::bail!("Unknown preprocessing preset: {}", name),
        }
    }

    /// Get preprocessing configuration for this preset
    pub fn to_config(self) -> PreprocessingConfig {
        match self {
            Self::ImageNet | Self::Coco => PreprocessingConfig {
                mean: [0.485, 0.456, 0.406],
                std: [0.229, 0.224, 0.225],
                channel_order: "RGB".to_string(),
            },
            Self::Yolo => PreprocessingConfig {
                mean: [0.0, 0.0, 0.0],
                std: [255.0, 255.0, 255.0],
                channel_order: "RGB".to_string(),
            },
            Self::Zero255 => PreprocessingConfig {
                mean: [0.0, 0.0, 0.0],
                std: [1.0, 1.0, 1.0],
                channel_order: "RGB".to_string(),
            },
        }
    }
}

/// HuggingFace preprocessor_config.json schema
#[derive(Debug, Clone, Deserialize)]
struct HFPreprocessorConfig {
    #[serde(default)]
    image_mean: Option<Vec<f32>>,
    #[serde(default)]
    image_std: Option<Vec<f32>>,
    #[serde(default)]
    size: Option<HFSize>,
    /// Some models (e.g., MobileNet) use crop_size instead of size
    #[serde(default)]
    crop_size: Option<HFSize>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum HFSize {
    Dict { height: u32, width: u32 },
    ShortestEdge { shortest_edge: u32 },
    Single(u32), // Square images
}

impl HFPreprocessorConfig {
    fn to_preprocessing(&self) -> PreprocessingConfig {
        PreprocessingConfig {
            mean: [
                self.image_mean.as_ref().and_then(|v| v.first()).copied().unwrap_or(0.0),
                self.image_mean.as_ref().and_then(|v| v.get(1)).copied().unwrap_or(0.0),
                self.image_mean.as_ref().and_then(|v| v.get(2)).copied().unwrap_or(0.0),
            ],
            std: [
                self.image_std.as_ref().and_then(|v| v.first()).copied().unwrap_or(1.0),
                self.image_std.as_ref().and_then(|v| v.get(1)).copied().unwrap_or(1.0),
                self.image_std.as_ref().and_then(|v| v.get(2)).copied().unwrap_or(1.0),
            ],
            channel_order: "RGB".to_string(),
        }
    }

    fn input_size(&self) -> Option<[u32; 2]> {
        // Check crop_size first (used by MobileNet, EfficientNet, etc.)
        // crop_size represents the actual model input dimensions
        if let Some(crop_size) = &self.crop_size {
            return match crop_size {
                HFSize::Dict { height, width } => Some([*width, *height]),
                HFSize::ShortestEdge { shortest_edge } => Some([*shortest_edge, *shortest_edge]),
                HFSize::Single(s) => Some([*s, *s]),
            };
        }

        // Fall back to size field
        match &self.size {
            Some(HFSize::Dict { height, width }) => Some([*width, *height]),
            Some(HFSize::ShortestEdge { shortest_edge }) => Some([*shortest_edge, *shortest_edge]),
            Some(HFSize::Single(s)) => Some([*s, *s]),
            None => None,
        }
    }
}

/// Installed model metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledModel {
    /// Model name
    pub name: String,
    /// Local file path
    pub path: PathBuf,
    /// File size in bytes
    pub size: u64,
    /// Catalog entry if from catalog
    pub catalog_entry: Option<ModelCatalogEntry>,
}

/// Model management service
pub struct ModelService {
    /// HuggingFace API client
    api: Api,
    /// Model catalog
    catalog: Vec<ModelCatalogEntry>,
    /// Models directory (defaults to ./models)
    models_dir: PathBuf,
}

impl ModelService {
    /// Create a new ModelService with default settings
    #[allow(dead_code)]
    pub fn new() -> Result<Self> {
        Self::with_models_dir(PathBuf::from("models"))
    }

    /// Create a ModelService with custom models directory
    pub fn with_models_dir(models_dir: PathBuf) -> Result<Self> {
        let api = Api::new().context("Failed to initialize HuggingFace API")?;

        // Load embedded model catalog
        let catalog_toml = include_str!("../../model_catalog.toml");
        let catalog: ModelCatalog = toml::from_str(catalog_toml).context("Failed to parse model_catalog.toml")?;

        Ok(Self {
            api,
            catalog: catalog.models,
            models_dir,
        })
    }

    /// List all models in the catalog
    pub fn list_catalog(&self) -> Result<Vec<ModelCatalogEntry>> {
        Ok(self.catalog.clone())
    }

    /// Get catalog entry for a model by name
    pub fn get_catalog_entry(&self, name: &str) -> Option<&ModelCatalogEntry> {
        self.catalog.iter().find(|m| m.name == name)
    }

    /// List all installed models
    pub async fn list_installed(&self) -> Result<Vec<InstalledModel>> {
        // Ensure models directory exists
        if !self.models_dir.exists() {
            return Ok(Vec::new());
        }

        let mut installed = Vec::new();
        let mut entries = fs::read_dir(&self.models_dir).await?;

        while let Some(entry) = entries.next_entry().await? {
            let path = entry.path();

            // Only look at directories (each model has its own directory)
            if !path.is_dir() {
                continue;
            }

            // Check if model.onnx exists in this directory
            let model_path = path.join("model.onnx");
            if !model_path.exists() {
                continue;
            }

            let metadata = fs::metadata(&model_path).await?;
            let size = metadata.len();

            // Extract model name from directory name
            let name = path
                .file_name()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown")
                .to_string();

            // Try to find catalog entry
            let catalog_entry = self.get_catalog_entry(&name).cloned();

            installed.push(InstalledModel {
                name,
                path: model_path,
                size,
                catalog_entry,
            });
        }

        Ok(installed)
    }

    /// Download a model from the catalog
    pub async fn pull(&self, name: &str, progress: Option<&ProgressBar>) -> Result<PathBuf> {
        // Find catalog entry
        let entry = self
            .get_catalog_entry(name)
            .context(format!("Model '{}' not found in catalog", name))?;

        // Create model directory: models/<name>/
        let model_dir = self.models_dir.join(name);
        fs::create_dir_all(&model_dir).await?;

        // Download model.onnx
        let model_path = self
            .pull_from_repo(&entry.repo, &entry.filename, name, progress)
            .await?;

        // Download labels file or write inline classes
        if !entry.classes.is_empty() {
            // Write inline classes to labels.txt
            self.write_inline_labels(name, &entry.classes).await?;
        } else if entry.task == "object-detection" {
            // Try to download labels.json from HuggingFace repo (for COCO models)
            self.pull_labels_from_repo(entry, name, progress).await?;
        } else if entry.task == "image-classification" {
            // Try to download ImageNet labels for classification models
            self.pull_labels_file(name, "imagenet-labels.txt", progress).await?;
        }

        // Generate and write config.json with auto-detection
        self.generate_model_config(entry, &model_path, progress).await?;

        // Auto-quantize if configured
        if let Some(quantize_config) = &entry.quantize {
            if quantize_config.enabled {
                self.quantize_model(&model_path, quantize_config, progress).await?;
            }
        }

        if let Some(pb) = progress {
            pb.set_message(format!("✅ Model '{}' ready at {}", name, model_dir.display()));
        }

        Ok(model_path)
    }

    /// Download a custom model from a HuggingFace repository
    pub async fn pull_from_repo(
        &self,
        repo: &str,
        filename: &str,
        name: &str,
        progress: Option<&ProgressBar>,
    ) -> Result<PathBuf> {
        // Create model directory: models/<name>/
        let model_dir = self.models_dir.join(name);
        fs::create_dir_all(&model_dir).await?;

        // Output path: models/<name>/model.onnx
        let output_path = model_dir.join("model.onnx");

        // Check if already downloaded
        if output_path.exists() {
            if let Some(pb) = progress {
                pb.set_message(format!("Model '{}' already cached", name));
            }
            return Ok(output_path);
        }

        // Download from HuggingFace
        if let Some(pb) = progress {
            pb.set_style(
                ProgressStyle::default_spinner()
                    .template("{spinner:.green} {msg}")
                    .unwrap(),
            );
            pb.set_message(format!("Downloading {} from {}", name, repo));
        }

        // Use HF API to download file
        let repo_api = self.api.model(repo.to_string());
        let hf_cached_path = repo_api
            .get(filename)
            .await
            .context(format!("Failed to download {} from {}", filename, repo))?;

        // Copy from HF cache to project models directory
        fs::copy(&hf_cached_path, &output_path)
            .await
            .context("Failed to copy model to project directory")?;

        if let Some(pb) = progress {
            pb.set_message(format!("Downloaded {} successfully", name));
        }

        Ok(output_path)
    }

    /// Download a labels file (e.g., imagenet-labels.txt) into model directory
    async fn pull_labels_file(
        &self,
        model_name: &str,
        filename: &str,
        progress: Option<&ProgressBar>,
    ) -> Result<PathBuf> {
        // Output path: models/<name>/labels.txt
        let model_dir = self.models_dir.join(model_name);
        let output_path = model_dir.join("labels.txt");

        // Check if already downloaded
        if output_path.exists() {
            if let Some(pb) = progress {
                pb.set_message("Labels file already cached".to_string());
            }
            return Ok(output_path);
        }

        // Map known label files to download URLs
        let url = match filename {
            "imagenet-labels.txt" => "https://raw.githubusercontent.com/pytorch/hub/master/imagenet_classes.txt",
            _ => {
                // Try to download from HuggingFace or skip if unknown
                if let Some(pb) = progress {
                    pb.set_message(format!("⚠️  Unknown labels file: {}, skipping", filename));
                }
                return Ok(output_path); // Return path even if not downloaded
            }
        };

        if let Some(pb) = progress {
            pb.set_message(format!("Downloading labels: {}", filename));
        }

        // Download using reqwest
        let client = reqwest::Client::new();
        let response = client
            .get(url)
            .send()
            .await
            .context(format!("Failed to download labels from {}", url))?;

        if !response.status().is_success() {
            anyhow::bail!("Failed to download labels: HTTP {}", response.status());
        }

        let content = response.text().await.context("Failed to read labels content")?;

        // Write to file
        fs::write(&output_path, content)
            .await
            .context("Failed to write labels file")?;

        if let Some(pb) = progress {
            pb.set_message(format!("Downloaded labels: {}", filename));
        }

        Ok(output_path)
    }

    /// Write inline classes to labels.txt
    async fn write_inline_labels(&self, model_name: &str, classes: &[String]) -> Result<()> {
        let model_dir = self.models_dir.join(model_name);
        let labels_path = model_dir.join("labels.txt");

        let content = classes.join("\n");
        fs::write(&labels_path, content)
            .await
            .context("Failed to write inline labels to labels.txt")?;

        Ok(())
    }

    /// Download labels.json from HuggingFace repo and convert to labels.txt
    async fn pull_labels_from_repo(
        &self,
        entry: &ModelCatalogEntry,
        model_name: &str,
        progress: Option<&ProgressBar>,
    ) -> Result<()> {
        let model_dir = self.models_dir.join(model_name);
        let labels_path = model_dir.join("labels.txt");

        // Check if already downloaded
        if labels_path.exists() {
            if let Some(pb) = progress {
                pb.set_message("Labels file already cached".to_string());
            }
            return Ok(());
        }

        // Extract directory path from filename (e.g., "yolov8n/model.onnx" -> "yolov8n")
        let model_dir_in_repo = entry.filename.rsplit_once('/').map(|(dir, _)| dir).unwrap_or("");

        if model_dir_in_repo.is_empty() {
            // No labels.json in repo root, skip
            return Ok(());
        }

        let labels_filename = format!("{}/labels.json", model_dir_in_repo);

        if let Some(pb) = progress {
            pb.set_message(format!("Downloading labels from {}", entry.repo));
        }

        // Download labels.json directly via HTTP
        let url = format!("https://huggingface.co/{}/raw/main/{}", entry.repo, labels_filename);

        let client = reqwest::Client::new();
        let response = match client.get(&url).send().await {
            Ok(resp) if resp.status().is_success() => resp,
            _ => {
                // labels.json not found, skip silently (model might use inline classes)
                return Ok(());
            }
        };

        let json_content = response.text().await.context("Failed to read labels.json response")?;

        let labels: Vec<String> = serde_json::from_str(&json_content).context("Failed to parse labels.json")?;

        // Write to labels.txt (one class per line)
        let content = labels.join("\n");
        fs::write(&labels_path, content)
            .await
            .context("Failed to write labels.txt")?;

        if let Some(pb) = progress {
            pb.set_message(format!("Downloaded {} class labels", labels.len()));
        }

        Ok(())
    }

    /// Fetch preprocessor_config.json from HuggingFace repo
    async fn fetch_hf_preprocessor_config(&self, repo: &str) -> Result<HFPreprocessorConfig> {
        let url = format!("https://huggingface.co/{}/raw/main/preprocessor_config.json", repo);

        let client = reqwest::Client::new();
        let response = client
            .get(&url)
            .send()
            .await
            .context(format!("Failed to fetch from {}", url))?;

        if !response.status().is_success() {
            anyhow::bail!(
                "HuggingFace preprocessor_config.json not found for {} (HTTP {})",
                repo,
                response.status()
            );
        }

        let config: HFPreprocessorConfig = response
            .json()
            .await
            .context("Failed to parse preprocessor_config.json")?;

        Ok(config)
    }

    /// Extract input size from ONNX model metadata
    ///
    /// Note: This is best-effort and may fail depending on the ONNX model format.
    /// Falls back to None if extraction fails.
    fn extract_input_size_from_onnx(&self, model_path: &Path) -> Option<[u32; 2]> {
        use ort::session::Session;

        // Best-effort extraction - don't fail if ONNX model is not parseable
        let session = Session::builder().ok()?.commit_from_file(model_path).ok()?;

        // Get first input tensor
        let _input = session.inputs.first()?;

        // Try to extract dimensions from the input type
        // Note: The ort API has changed between versions, so this is best-effort
        // For now, we'll return None and rely on HF config or presets
        // TODO: Update when ort API is stabilized

        None
    }

    /// Auto-detect preprocessing parameters with fallback priority:
    /// 1. HuggingFace preprocessor_config.json
    /// 2. Preprocessing preset from catalog
    /// 3. ONNX metadata (input size only, use sensible defaults for mean/std)
    async fn auto_detect_preprocessing(
        &self,
        entry: &ModelCatalogEntry,
        model_path: &Path,
        progress: Option<&ProgressBar>,
    ) -> Result<(PreprocessingConfig, [u32; 2])> {
        // Try HuggingFace preprocessor_config.json first
        if let Some(pb) = progress {
            pb.set_message(format!("🔍 Auto-detecting preprocessing for {}", entry.name));
        }

        if let Ok(hf_config) = self.fetch_hf_preprocessor_config(&entry.repo).await {
            tracing::debug!(
                "HF config: size={:?}, crop_size={:?}",
                hf_config.size,
                hf_config.crop_size
            );

            let preprocessing = hf_config.to_preprocessing();
            let input_size = hf_config.input_size().unwrap_or([224, 224]);

            tracing::debug!(
                "Detected preprocessing: mean={:?}, std={:?}, input_size={:?}",
                preprocessing.mean,
                preprocessing.std,
                input_size
            );

            if let Some(pb) = progress {
                pb.set_message(format!(
                    "✅ Auto-detected from HuggingFace (input_size={:?})",
                    input_size
                ));
            }

            return Ok((preprocessing, input_size));
        } else {
            tracing::debug!("Failed to fetch HuggingFace preprocessor config, falling back to preset");
        }

        // Try preprocessing preset
        if let Some(preset_name) = &entry.preprocessing_preset {
            if let Ok(preset) = PreprocessingPreset::from_name(preset_name) {
                let preprocessing = preset.to_config();

                // Try to get input_size from ONNX (best-effort)
                let input_size = self.extract_input_size_from_onnx(model_path).unwrap_or([224, 224]);

                if let Some(pb) = progress {
                    pb.set_message(format!(
                        "✅ Using preset '{}' (input_size={:?})",
                        preset_name, input_size
                    ));
                }

                return Ok((preprocessing, input_size));
            }
        }

        // Fallback: Try ONNX metadata for input_size, use sensible defaults
        let input_size = self.extract_input_size_from_onnx(model_path).unwrap_or([224, 224]);

        let preprocessing = PreprocessingConfig {
            mean: [0.0, 0.0, 0.0],
            std: [1.0, 1.0, 1.0],
            channel_order: "RGB".to_string(),
        };

        if let Some(pb) = progress {
            pb.set_message(format!(
                "⚠️  Using fallback preprocessing (input_size={:?}). Consider editing config.json",
                input_size
            ));
        }

        Ok((preprocessing, input_size))
    }

    /// Generate and write config.json for a model with auto-detected values
    async fn generate_model_config(
        &self,
        entry: &ModelCatalogEntry,
        model_path: &Path,
        progress: Option<&ProgressBar>,
    ) -> Result<()> {
        let model_dir = self.models_dir.join(&entry.name);
        let config_path = model_dir.join("config.json");

        // Auto-detect preprocessing
        let (preprocessing, input_size) = self.auto_detect_preprocessing(entry, model_path, progress).await?;

        // Determine num_classes
        let num_classes = if entry.task == "object-detection" {
            entry.classes.len().max(1)
        } else {
            1000 // Default for image classification (ImageNet)
        };

        // Convert catalog entry to ModelConfig
        let config = ModelConfig {
            name: entry.name.clone(),
            task: entry.task.clone(),
            repo: entry.repo.clone(),
            filename: entry.filename.clone(),
            input_size,
            preprocessing,
            num_classes,
            labels_file: "labels.txt".to_string(),
            custom_labels: CustomLabelsConfig::default(),
        };

        // Write config as pretty JSON
        let json = serde_json::to_string_pretty(&config).context("Failed to serialize model config")?;

        fs::write(&config_path, json)
            .await
            .context("Failed to write model config.json")?;

        if let Some(pb) = progress {
            pb.set_message(format!("📝 Wrote config to {}", config_path.display()));
        }

        Ok(())
    }

    /// Quantize a model to INT8 (calls embedded Python script)
    async fn quantize_model(
        &self,
        model_path: &Path,
        config: &QuantizeConfig,
        progress: Option<&ProgressBar>,
    ) -> Result<PathBuf> {
        let int8_path = model_path.with_file_name("model-int8.onnx");

        // Skip if already quantized
        if int8_path.exists() {
            if let Some(pb) = progress {
                pb.set_message("INT8 model already cached");
            }
            return Ok(int8_path);
        }

        if let Some(pb) = progress {
            pb.set_message("Quantizing model to INT8...");
        }

        match config.method.as_str() {
            "dynamic_int8" => {
                self.quantize_dynamic_int8(model_path, &int8_path).await?;
            }
            _ => {
                anyhow::bail!("Unsupported quantization method: {}", config.method);
            }
        }

        if let Some(pb) = progress {
            pb.set_message("✅ INT8 model ready");
        }

        Ok(int8_path)
    }

    /// Perform dynamic INT8 quantization using ONNX Runtime tools
    async fn quantize_dynamic_int8(&self, input: &Path, output: &Path) -> Result<()> {
        // Check if Python + onnxruntime available
        let python = self.find_python()?;

        // Embed quantization script in binary
        let script = include_str!("../../scripts/quantize_int8.py");
        let script_path = std::env::temp_dir().join("mecha10_quantize_int8.py");
        fs::write(&script_path, script).await?;

        // Run Python quantization script
        let output_result = tokio::process::Command::new(&python)
            .arg(&script_path)
            .arg(input)
            .arg(output)
            .output()
            .await?;

        // Cleanup temp script
        let _ = fs::remove_file(&script_path).await;

        if !output_result.status.success() {
            let stderr = String::from_utf8_lossy(&output_result.stderr);
            anyhow::bail!(
                "Quantization failed: {}\n\nTip: Install with 'pip install onnx onnxruntime'",
                stderr
            );
        }

        Ok(())
    }

    /// Find Python 3 executable
    fn find_python(&self) -> Result<String> {
        for candidate in &["python3", "python"] {
            if which::which(candidate).is_ok() {
                return Ok(candidate.to_string());
            }
        }
        anyhow::bail!("Python 3 not found. Install with: brew install python3 (macOS) or apt install python3 (Linux)")
    }

    /// Remove an installed model
    pub async fn remove(&self, name: &str) -> Result<()> {
        let model_dir = self.models_dir.join(name);

        if !model_dir.exists() {
            anyhow::bail!("Model '{}' is not installed", name);
        }

        fs::remove_dir_all(&model_dir)
            .await
            .context(format!("Failed to remove model '{}'", name))?;

        Ok(())
    }

    /// Get the path to a model's ONNX file
    ///
    /// Note: This method is no longer used by CLI (replaced by node-runner in Phase 2).
    /// Kept for potential future use.
    #[allow(dead_code)]
    pub fn get_model_path(&self, name: &str) -> PathBuf {
        self.models_dir.join(name).join("model.onnx")
    }

    /// Check if a model is installed
    ///
    /// Note: This method is no longer used by CLI (replaced by node-runner in Phase 2).
    /// Kept for potential future use.
    #[allow(dead_code)]
    pub async fn is_installed(&self, name: &str) -> bool {
        let model_path = self.get_model_path(name);
        model_path.exists()
    }

    /// Get info about a model (catalog or installed)
    pub async fn info(&self, name: &str) -> Result<ModelInfo> {
        let catalog_entry = self.get_catalog_entry(name).cloned();
        let installed = self.list_installed().await?;
        let installed_info = installed.iter().find(|m| m.name == name).cloned();

        Ok(ModelInfo {
            name: name.to_string(),
            catalog_entry,
            installed_info,
        })
    }

    /// Validate a model file is a valid ONNX file
    #[allow(dead_code)]
    pub async fn validate(&self, path: &Path) -> Result<bool> {
        // Basic validation: check file exists and has .onnx extension
        if !path.exists() {
            return Ok(false);
        }

        if path.extension().and_then(|s| s.to_str()) != Some("onnx") {
            return Ok(false);
        }

        // TODO: Add ONNX format validation using ort crate
        // For now, just check the magic bytes
        let bytes = fs::read(path).await?;

        // ONNX files are Protocol Buffers, check for protobuf signature
        // This is a very basic check - full validation would require parsing
        Ok(bytes.len() > 4)
    }
}

/// Combined model information (catalog + installed)
#[derive(Debug, Clone, Serialize)]
pub struct ModelInfo {
    pub name: String,
    pub catalog_entry: Option<ModelCatalogEntry>,
    pub installed_info: Option<InstalledModel>,
}

impl ModelInfo {
    /// Check if model is installed
    #[allow(dead_code)]
    pub fn is_installed(&self) -> bool {
        self.installed_info.is_some()
    }

    /// Check if model is in catalog
    #[allow(dead_code)]
    pub fn is_in_catalog(&self) -> bool {
        self.catalog_entry.is_some()
    }
}
