//! Product registry for loading and querying product definitions.
//!
//! The registry loads product definitions from YAML manifests
//! and provides lookup by name, source, or collection.
use super::types::ProductDef;
use crate::qvf::Collection;
use anyhow::{anyhow, Result};
use serde_yaml;
use std::collections::HashMap;
use std::path::Path;

/// In-memory registry of product definitions.
///
/// Products are loaded from YAML files and indexed by name.
/// Supports lookup by name, source, and collection.
///
/// # Example
///
/// ```ignore
/// use rss_core::products::ProductRegistry;
///
/// let registry = ProductRegistry::load_from_dir("data/products")?;
/// let product = registry.get("dea_sentinel2_ard")?;
/// println!("Measurements: {:?}", product.measurement_names());
/// ```
pub struct ProductRegistry {
    products: HashMap<String, ProductDef>,
}

impl ProductRegistry {
    /// Creates an empty registry.
    pub fn new() -> Self {
        Self {
            products: HashMap::new(),
        }
    }

    /// Loads all YAML product definitions from a directory.
    ///
    /// Reads all `.yaml` and `.yml` files in the given directory
    /// and parses them as [`ProductDef`] instances.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The directory cannot be read
    /// - A YAML file contains invalid syntax
    /// - A product definition is missing required fields
    pub fn load_from_dir<P: AsRef<Path>>(path: P) -> Result<Self> {
        let mut registry = Self::new();
        let dir = path.as_ref();

        if !dir.is_dir() {
            return Err(anyhow!("Path is not a directory: {:?}", dir));
        }

        for entry in std::fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_file() {
                if let Some(ext) = path.extension() {
                    if ext == "yaml" || ext == "yml" {
                        let contents = std::fs::read_to_string(&path)?;
                        let product: ProductDef = serde_yaml::from_str(&contents)?;
                        registry.add(product);
                    }
                }
            }
        }

        Ok(registry)
    }

    /// Adds a product definition to the registry.
    ///
    /// If a product with the same name already exists, it will be overwritten.
    pub fn add(&mut self, product: ProductDef) {
        self.products.insert(product.name.clone(), product);
    }

    /// Looks up a product by name.
    ///
    /// # Errors
    ///
    /// Returns an error if no product with the given name exists.
    pub fn get(&self, name: &str) -> Result<&ProductDef> {
        self.products
            .get(name)
            .ok_or_else(|| anyhow!("Product not found: {}", name))
    }

    /// Returns all products matching the given source string.
    pub fn by_source(&self, source: &str) -> Vec<&ProductDef> {
        self.products
            .values()
            .filter(|p| p.source == source)
            .collect()
    }

    /// Looks up a product by source and collection.
    ///
    /// Returns the STAC collection ID string (may contain comma-separated IDs).
    ///
    /// # Errors
    ///
    /// Returns an error if no matching product exists.
    pub fn get_stac_collection(
        &self,
        source: &str,
        collection: Collection,
    ) -> Result<Option<&str>> {
        let products: Vec<&ProductDef> = self
            .products
            .values()
            .filter(|p| p.source == source && p.collection == collection)
            .collect();

        if products.is_empty() {
            return Ok(None);
        }

        // Return the stac_collection field (may be empty string for Apollo)
        Ok(Some(products[0].stac_collection.as_str()))
    }

    /// Looks up a full product definition by source name and collection.
    ///
    /// Returns the complete [`ProductDef`] including measurements, cloud mask
    /// configuration, and STAC collection IDs.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use rss_core::{PRODUCT_REGISTRY, qvf::Collection};
    ///
    /// let product = PRODUCT_REGISTRY
    ///     .by_source_and_collection("DEA", Collection::Sentinel2)?;
    /// println!("Measurements: {:?}", product.measurement_names());
    /// println!("Bands: {:?}", product.band_ids());
    /// ```
    ///
    /// # Errors
    ///
    /// Returns an error if no matching product exists.
    pub fn by_source_and_collection(
        &self,
        source: &str,
        collection: Collection,
    ) -> Result<&ProductDef> {
        self.products
            .values()
            .find(|p| p.source == source && p.collection == collection)
            .ok_or_else(|| {
                anyhow!(
                    "No product found for source={}, collection={:?}",
                    source,
                    collection
                )
            })
    }

    /// Returns all product names in the registry.
    pub fn names(&self) -> Vec<&str> {
        self.products.keys().map(|s| s.as_str()).collect()
    }

    /// Returns the total number of products in the registry.
    pub fn len(&self) -> usize {
        self.products.len()
    }

    /// Returns true if the registry contains no products.
    pub fn is_empty(&self) -> bool {
        self.products.is_empty()
    }
}

impl Default for ProductRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::qvf::Collection;
    use crate::products::types::MeasurementDef;
    use std::fs;
    use tempfile::TempDir;

    fn make_test_product(name: &str, source: &str) -> ProductDef {
        ProductDef {
            name: name.to_string(),
            title: format!("Test {}", name),
            description: String::new(),
            source: source.to_string(),
            collection: Collection::Sentinel2,
            stac_collection: String::new(),
            native_resolution: 10.0,
            measurements: vec![MeasurementDef {
                name: "red".to_string(),
                band: "B04".to_string(),
                canonical: None,
                description: String::new(),
                units: "1".to_string(),
                kind: "measurement".to_string(),
            }],
            cloud_mask: None,
        }
    }

    #[test]
    fn test_registry_new_is_empty() {
        let registry = ProductRegistry::new();
        assert!(registry.is_empty());
        assert_eq!(registry.len(), 0);
    }

    #[test]
    fn test_registry_add_and_get() {
        let mut registry = ProductRegistry::new();
        let product = make_test_product("test_prod", "DEA");
        registry.add(product);

        assert_eq!(registry.len(), 1);
        assert!(!registry.is_empty());

        let got = registry.get("test_prod").expect("Product not found");
        assert_eq!(got.title, "Test test_prod");
    }

    #[test]
    fn test_registry_get_missing() {
        let registry = ProductRegistry::new();
        let result = registry.get("nonexistent");
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("Product not found"));
    }

    #[test]
    fn test_registry_names() {
        let mut registry = ProductRegistry::new();
        registry.add(make_test_product("prod_a", "DEA"));
        registry.add(make_test_product("prod_b", "Element84"));

        let mut names = registry.names();
        names.sort();
        assert_eq!(names, vec!["prod_a", "prod_b"]);
    }

    #[test]
    fn test_registry_by_source() {
        let mut registry = ProductRegistry::new();
        registry.add(make_test_product("dea_1", "DEA"));
        registry.add(make_test_product("dea_2", "DEA"));
        registry.add(make_test_product("e84_1", "Element84"));

        let dea_products = registry.by_source("DEA");
        assert_eq!(dea_products.len(), 2);

        let e84_products = registry.by_source("Element84");
        assert_eq!(e84_products.len(), 1);

        let missing = registry.by_source("Missing");
        assert!(missing.is_empty());
    }

    #[test]
    fn test_registry_overwrite() {
        let mut registry = ProductRegistry::new();
        registry.add(make_test_product("test", "DEA"));
        let product2 = make_test_product("test", "Element84");
        registry.add(product2);

        assert_eq!(registry.len(), 1);
        let got = registry.get("test").expect("Product not found");
        assert_eq!(got.source, "Element84");
    }

    #[test]
    fn test_load_from_dir_not_found() {
        let result = ProductRegistry::load_from_dir("/nonexistent/path");
        assert!(result.is_err());
    }

    #[test]
    fn test_load_from_dir_empty() {
        let dir = TempDir::new().expect("Failed to create temp dir");
        let registry = ProductRegistry::load_from_dir(dir.path()).expect("Failed to load");
        assert!(registry.is_empty());
    }

    #[test]
    fn test_load_from_dir_valid_yaml() {
        let dir = TempDir::new().expect("Failed to create temp dir");

        let yaml_content = r#"
name: test_s2
title: "Test Sentinel-2"
source: DEA
collection: sentinel2
native_resolution: 10
measurements:
  - name: red
    band: B04
  - name: nir
    band: B08
cloud_mask:
  property: "cloud_cover"
  threshold: 10
"#;
        fs::write(dir.path().join("test_s2.yaml"), yaml_content)
            .expect("Failed to write YAML");

        let registry = ProductRegistry::load_from_dir(dir.path()).expect("Failed to load");
        assert_eq!(registry.len(), 1);

        let product = registry.get("test_s2").expect("Product not found");
        assert_eq!(product.source, "DEA");
        assert_eq!(product.collection, Collection::Sentinel2);
        assert_eq!(product.native_resolution, 10.0);
        assert_eq!(product.measurements.len(), 2);
        assert!(product.cloud_mask.is_some());
    }

    #[test]
    fn test_load_from_dir_multiple_files() {
        let dir = TempDir::new().expect("Failed to create temp dir");

        fs::write(
            dir.path().join("s2.yaml"),
            "name: s2_prod\nsource: DEA\ncollection: sentinel2\nmeasurements: []",
        )
        .expect("Failed to write");

        fs::write(
            dir.path().join("ls8.yml"),
            "name: ls8_prod\nsource: Element84\ncollection: landsat8\nmeasurements: []",
        )
        .expect("Failed to write");

        // Non-YAML file should be ignored
        fs::write(dir.path().join("readme.txt"), "not a product")
            .expect("Failed to write");

        let registry = ProductRegistry::load_from_dir(dir.path()).expect("Failed to load");
        assert_eq!(registry.len(), 2);
        assert!(registry.get("s2_prod").is_ok());
        assert!(registry.get("ls8_prod").is_ok());
    }

    #[test]
    fn test_load_from_dir_invalid_yaml() {
        let dir = TempDir::new().expect("Failed to create temp dir");
        fs::write(dir.path().join("bad.yaml"), "this: is: bad: yaml")
            .expect("Failed to write");

        let result = ProductRegistry::load_from_dir(dir.path());
        assert!(result.is_err());
    }

    #[test]
    fn test_registry_default() {
        let registry = ProductRegistry::default();
        assert!(registry.is_empty());
    }

    #[test]
    fn test_get_stac_collection_found() {
        let mut registry = ProductRegistry::new();
        let mut product = make_test_product("dea_s2", "DEA");
        product.stac_collection = "ga_s2am_ard_3,ga_s2bm_ard_3".to_string();
        registry.add(product);

        let result = registry
            .get_stac_collection("DEA", Collection::Sentinel2)
            .expect("Failed to lookup");
        assert!(result.is_some());
        assert_eq!(result.unwrap(), "ga_s2am_ard_3,ga_s2bm_ard_3");
    }

    #[test]
    fn test_get_stac_collection_not_found() {
        let registry = ProductRegistry::new();
        let result = registry
            .get_stac_collection("Missing", Collection::Sentinel2)
            .expect("Failed to lookup");
        assert!(result.is_none());
    }

    #[test]
    fn test_get_stac_collection_empty_string() {
        let mut registry = ProductRegistry::new();
        let mut product = make_test_product("apollo_s2", "Apollo");
        product.stac_collection = String::new(); // Apollo has no STAC collection
        registry.add(product);

        let result = registry
            .get_stac_collection("Apollo", Collection::Sentinel2)
            .expect("Failed to lookup");
        assert!(result.is_some());
        assert_eq!(result.unwrap(), "");
    }

    #[test]
    fn test_by_source_and_collection_found() {
        let mut registry = ProductRegistry::new();
        let mut product = make_test_product("dea_s2", "DEA");
        product.stac_collection = "ga_s2am_ard_3".to_string();
        registry.add(product);

        let found = registry
            .by_source_and_collection("DEA", Collection::Sentinel2)
            .expect("Failed to lookup");
        assert_eq!(found.name, "dea_s2");
        assert_eq!(found.stac_collection, "ga_s2am_ard_3");
    }

    #[test]
    fn test_by_source_and_collection_not_found() {
        let registry = ProductRegistry::new();
        let result = registry.by_source_and_collection("DEA", Collection::Sentinel2);
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("No product found"));
    }

    #[test]
    fn test_load_all_manifests_from_data_dir() {
        // Integration test: load all YAML manifests from the data/products directory
        let manifest_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/data/products");
        let registry =
            ProductRegistry::load_from_dir(manifest_dir).expect("Failed to load product manifests");

        // Should have loaded all manifests
        assert!(
            registry.len() >= 11,
            "Expected at least 11 products, got {}",
            registry.len()
        );

        // Verify expected products exist
        let expected = [
            "dea_sentinel2_ard",
            "dea_landsat_all_ard",
            "dea_landsat7_ard",
            "dea_landsat8_ard",
            "dea_landsat9_ard",
            "element84_sentinel2",
            "element84_landsat8",
            "element84_landsat9",
            "pc_sentinel2",
            "pc_landsat8",
            "pc_landsat9",
        ];

        for name in &expected {
            let product = registry
                .get(name)
                .unwrap_or_else(|_| panic!("Expected product {} not found", name));
            assert!(!product.title.is_empty(), "Product {} has empty title", name);
            assert!(
                !product.measurements.is_empty(),
                "Product {} has no measurements",
                name
            );
            assert!(
                product.cloud_mask.is_some(),
                "Product {} has no cloud mask",
                name
            );
            assert!(
                !product.stac_collection.is_empty(),
                "Product {} has empty stac_collection",
                name
            );
        }

        // Verify stac_collection IDs are correct
        let dea_s2 = registry.get("dea_sentinel2_ard").expect("DEA S2 not found");
        assert_eq!(dea_s2.stac_collection, "ga_s2am_ard_3,ga_s2bm_ard_3,ga_s2cm_ard_3");

        let e84_s2 = registry.get("element84_sentinel2").expect("E84 S2 not found");
        assert_eq!(e84_s2.stac_collection, "sentinel-2-l2a");

        let pc_ls8 = registry.get("pc_landsat8").expect("PC LS8 not found");
        assert_eq!(pc_ls8.stac_collection, "landsat-c2-l2");

        // Verify source grouping
        let dea_products = registry.by_source("DEA");
        assert_eq!(dea_products.len(), 5, "Expected 5 DEA products");

        let e84_products = registry.by_source("Element84");
        assert_eq!(e84_products.len(), 3, "Expected 3 Element84 products");

        let pc_products = registry.by_source("PlanetaryComputer");
        assert_eq!(pc_products.len(), 3, "Expected 3 PlanetaryComputer products");
    }

    #[test]
    fn test_landsat_all_product_exists_and_resolves() {
        // Integration test: dea_landsat_all_ard should be loadable and resolve STAC collections
        let manifest_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/data/products");
        let registry =
            ProductRegistry::load_from_dir(manifest_dir).expect("Failed to load product manifests");

        // Verify the combined LandsatAll product exists
        let landsat_all = registry
            .get("dea_landsat_all_ard")
            .expect("dea_landsat_all_ard not found in registry");

        assert_eq!(landsat_all.source, "DEA");
        assert_eq!(landsat_all.collection, Collection::LandsatAll);
        assert_eq!(landsat_all.stac_collection, "ga_ls7e_ard_3,ga_ls8c_ard_3,ga_ls9c_ard_3");

        // Verify STAC collection lookup works for DEA + LandsatAll
        let stac = registry
            .get_stac_collection("DEA", Collection::LandsatAll)
            .expect("Failed to get STAC collection")
            .expect("STAC collection should be Some");
        assert_eq!(stac, "ga_ls7e_ard_3,ga_ls8c_ard_3,ga_ls9c_ard_3");

        // Verify by_source_and_collection works for LandsatAll
        let resolved = registry
            .by_source_and_collection("DEA", Collection::LandsatAll)
            .expect("Should find DEA + LandsatAll product");
        assert_eq!(resolved.name, "dea_landsat_all_ard");

        // Verify band resolution works (same bands as individual L8/L9 products)
        assert_eq!(landsat_all.resolve_canonical("red"), Some("nbart_red"));
        assert_eq!(landsat_all.resolve_canonical("nir"), Some("nbart_nir"));
        assert_eq!(landsat_all.resolve_canonical(
            "swir_1"
        ), Some("nbart_swir_1"));

        // Verify cloud mask is configured
        let cm = landsat_all.cloud_mask.as_ref().expect("cloud_mask should be set");
        assert_eq!(cm.property, "dea:scene_cloud_cover");
    }
}
