//! Core types for product definitions.
//!
//! Defines the schema for product manifest YAML files.
use crate::qvf::Collection;
use serde::{Deserialize, Serialize};

/// A single measurement (band) within a product.
///
/// The optional `canonical` field provides a semantic name for cross-provider
/// band resolution. For example, DEA's `nbart_red` and Element84's `band4`
/// both map to canonical name `red` (band B04 on Sentinel-2).
///
/// Example from YAML:
/// ```yaml
/// - name: nbart_red
///   band: B04
///   canonical: red
///   description: "NBAR reflectance, red"
///   units: "1"
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MeasurementDef {
    /// Measurement name (used in queries and output)
    pub name: String,
    /// Band identifier in the source data (e.g., "B04", "B08")
    pub band: String,
    /// Canonical semantic name for cross-provider resolution (e.g., "red", "nir")
    #[serde(default)]
    pub canonical: Option<String>,
    /// Human-readable description
    #[serde(default)]
    pub description: String,
    /// Physical units (e.g., "1" for reflectance, "K" for temperature)
    #[serde(default = "default_units")]
    pub units: String,
    /// Kind of measurement: "measurement" (default) or "qa"
    #[serde(default = "default_kind")]
    pub kind: String,
}

fn default_units() -> String {
    "1".to_string()
}

fn default_kind() -> String {
    "measurement".to_string()
}

/// Cloud mask configuration for a product.
///
/// Example from YAML:
/// ```yaml
/// cloud_mask:
///   property: "dea:scene_cloud_cover"
///   threshold: 100
///   operator: less_than
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CloudMaskDef {
    /// STAC property name containing cloud cover percentage
    pub property: String,
    /// Maximum acceptable cloud cover percentage (0-100)
    #[serde(default = "default_threshold")]
    pub threshold: f64,
    /// Comparison operator for filtering
    #[serde(default = "default_operator")]
    pub operator: String,
}

fn default_threshold() -> f64 {
    100.0
}

fn default_operator() -> String {
    "less_than".to_string()
}

/// Full product definition loaded from a YAML manifest.
///
/// Example from YAML:
/// ```yaml
/// name: dea_sentinel2_ard
/// title: "DEA Sentinel-2 Analysis Ready Data"
/// source: DEA
/// collection: sentinel2
/// stac_collection: "ga_s2am_ard_3,ga_s2bm_ard_3"
/// native_resolution: 10
/// measurements:
///   - name: nbart_red
///     band: B04
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProductDef {
    /// Unique product identifier
    pub name: String,
    /// Human-readable title
    #[serde(default)]
    pub title: String,
    /// Description of the product
    #[serde(default)]
    pub description: String,
    /// Imagery source (DEA, Element84, PlanetaryComputer, Apollo)
    pub source: String,
    /// Collection type
    pub collection: Collection,
    /// STAC collection ID(s) for this product.
    /// Multiple IDs can be comma-separated (e.g., "ga_s2am_ard_3,ga_s2bm_ard_3").
    #[serde(default)]
    pub stac_collection: String,
    /// Native resolution in metres
    #[serde(default = "default_resolution")]
    pub native_resolution: f64,
    /// List of measurements (bands) available
    pub measurements: Vec<MeasurementDef>,
    /// Cloud mask configuration
    #[serde(default)]
    pub cloud_mask: Option<CloudMaskDef>,
}

fn default_resolution() -> f64 {
    30.0
}

impl ProductDef {
    /// Returns the measurement with the given name, if it exists.
    pub fn get_measurement(&self, name: &str) -> Option<&MeasurementDef> {
        self.measurements.iter().find(|m| m.name == name)
    }

    /// Returns the provider-specific measurement name for a canonical band name.
    ///
    /// Use this to resolve semantic band names (e.g., "red", "nir") to the
    /// provider-specific measurement name (e.g., "nbart_red" for DEA, "band4" for Element84).
    ///
    /// # Example
    ///
    /// ```ignore
    /// let product = PRODUCT_REGISTRY.get("dea_sentinel2_ard")?;
    /// let name = product.resolve_canonical("red");
    /// assert_eq!(name, Some("nbart_red"));
    /// ```
    pub fn resolve_canonical(&self, canonical: &str) -> Option<&str> {
        self.measurements
            .iter()
            .find(|m| m.canonical.as_deref() == Some(canonical))
            .map(|m| m.name.as_str())
    }

    /// Returns all measurement names.
    ///
    /// When `exclude_qa` is `true`, QA bands (kind = "qa") are filtered out.
    pub fn measurement_names(&self, exclude_qa: bool) -> Vec<&str> {
        self.measurements
            .iter()
            .filter(|m| !exclude_qa || m.kind != "qa")
            .map(|m| m.name.as_str())
            .collect()
    }

    /// Returns all band identifiers.
    pub fn band_ids(&self) -> Vec<&str> {
        self.measurements.iter().map(|m| m.band.as_str()).collect()
    }
}

impl CloudMaskDef {
    /// Checks if a cloud cover value passes the threshold.
    pub fn passes_threshold(&self, cloud_cover: f64) -> bool {
        match self.operator.as_str() {
            "less_than" | "lt" => cloud_cover < self.threshold,
            "less_than_equal" | "lte" | "less_equal" => cloud_cover <= self.threshold,
            "greater_than" | "gt" => cloud_cover > self.threshold,
            "greater_than_equal" | "gte" | "greater_equal" => cloud_cover >= self.threshold,
            "equal" | "eq" => (cloud_cover - self.threshold).abs() < f64::EPSILON,
            _ => true,
        }
    }
}

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

    #[test]
    fn test_measurement_def_serialization() {
        let yaml = r#"
name: nbart_red
band: B04
description: "NBAR reflectance, red"
units: "1"
"#;
        let m: MeasurementDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(m.name, "nbart_red");
        assert_eq!(m.band, "B04");
        assert_eq!(m.description, "NBAR reflectance, red");
        assert_eq!(m.units, "1");
    }

    #[test]
    fn test_measurement_def_defaults() {
        let yaml = r#"
name: band1
band: B01
"#;
        let m: MeasurementDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(m.description, "");
        assert_eq!(m.units, "1");
    }

    #[test]
    fn test_cloud_mask_def_serialization() {
        let yaml = r#"
property: "dea:scene_cloud_cover"
threshold: 10
operator: less_than
"#;
        let cm: CloudMaskDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(cm.property, "dea:scene_cloud_cover");
        assert_eq!(cm.threshold, 10.0);
        assert_eq!(cm.operator, "less_than");
    }

    #[test]
    fn test_cloud_mask_passes_threshold_less_than() {
        let cm = CloudMaskDef {
            property: "cloud_cover".to_string(),
            threshold: 20.0,
            operator: "less_than".to_string(),
        };
        assert!(cm.passes_threshold(10.0));
        assert!(!cm.passes_threshold(20.0));
        assert!(!cm.passes_threshold(30.0));
    }

    #[test]
    fn test_cloud_mask_passes_threshold_lte() {
        let cm = CloudMaskDef {
            property: "cloud_cover".to_string(),
            threshold: 20.0,
            operator: "less_than_equal".to_string(),
        };
        assert!(cm.passes_threshold(10.0));
        assert!(cm.passes_threshold(20.0));
        assert!(!cm.passes_threshold(30.0));
    }

    #[test]
    fn test_cloud_mask_def_defaults() {
        let yaml = r#"
property: "cloud_cover"
"#;
        let cm: CloudMaskDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(cm.threshold, 100.0);
        assert_eq!(cm.operator, "less_than");
    }

    #[test]
    fn test_product_def_serialization() {
        let yaml = r#"
name: dea_s2_test
title: "Test Product"
source: DEA
collection: sentinel2
native_resolution: 10
measurements:
  - name: nbart_red
    band: B04
cloud_mask:
  property: "cloud_cover"
  threshold: 10
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(p.name, "dea_s2_test");
        assert_eq!(p.title, "Test Product");
        assert_eq!(p.source, "DEA");
        assert_eq!(p.collection, Collection::Sentinel2);
        assert_eq!(p.native_resolution, 10.0);
        assert_eq!(p.measurements.len(), 1);
        assert_eq!(p.measurements[0].name, "nbart_red");
        assert_eq!(p.measurements[0].band, "B04");
        assert!(p.cloud_mask.is_some());
    }

    #[test]
    fn test_product_def_get_measurement() {
        let yaml = r#"
name: test_prod
source: DEA
collection: sentinel2
measurements:
  - name: red
    band: B04
  - name: nir
    band: B08
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert!(p.get_measurement("red").is_some());
        assert!(p.get_measurement("nir").is_some());
        assert!(p.get_measurement("swir").is_none());
    }

    #[test]
    fn test_product_def_measurement_names() {
        let yaml = r#"
name: test_prod
source: DEA
collection: sentinel2
measurements:
  - name: red
    band: B04
  - name: nir
    band: B08
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        let names = p.measurement_names(false);
        assert_eq!(names, vec!["red", "nir"]);
    }

    #[test]
    fn test_product_def_band_ids() {
        let yaml = r#"
name: test_prod
source: DEA
collection: sentinel2
measurements:
  - name: red
    band: B04
  - name: nir
    band: B08
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        let bands = p.band_ids();
        assert_eq!(bands, vec!["B04", "B08"]);
    }

    #[test]
    fn test_product_def_defaults() {
        let yaml = r#"
name: test_prod
source: DEA
collection: landsat8
measurements: []
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(p.title, "");
        assert_eq!(p.description, "");
        assert_eq!(p.native_resolution, 30.0);
        assert!(p.cloud_mask.is_none());
    }

    #[test]
    fn test_resolve_canonical_found() {
        let yaml = r#"
name: test_prod
source: DEA
collection: sentinel2
measurements:
  - name: nbart_red
    band: B04
    canonical: red
  - name: nbart_nir
    band: B08
    canonical: nir
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(p.resolve_canonical("red"), Some("nbart_red"));
        assert_eq!(p.resolve_canonical("nir"), Some("nbart_nir"));
    }

    #[test]
    fn test_resolve_canonical_not_found() {
        let yaml = r#"
name: test_prod
source: DEA
collection: sentinel2
measurements:
  - name: nbart_red
    band: B04
    canonical: red
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(p.resolve_canonical("nir"), None);
        assert_eq!(p.resolve_canonical("blue"), None);
    }

    #[test]
    fn test_resolve_canonical_no_canonical_field() {
        let yaml = r#"
name: test_prod
source: DEA
collection: sentinel2
measurements:
  - name: band1
    band: B01
"#;
        let p: ProductDef = serde_yaml::from_str(yaml).expect("Failed to parse");
        assert_eq!(p.resolve_canonical("coastal_aerosol"), None);
    }

    #[test]
    fn test_resolve_canonical_cross_provider() {
        // Simulate DEA product
        let dea_yaml = r#"
name: dea_s2
source: DEA
collection: sentinel2
measurements:
  - name: nbart_red
    band: B04
    canonical: red
"#;
        let dea: ProductDef = serde_yaml::from_str(dea_yaml).expect("Failed to parse");

        // Simulate Element84 product
        let e84_yaml = r#"
name: e84_s2
source: Element84
collection: sentinel2
measurements:
  - name: band4
    band: B04
    canonical: red
"#;
        let e84: ProductDef = serde_yaml::from_str(e84_yaml).expect("Failed to parse");

        // Both should resolve "red" to their respective band names
        assert_eq!(dea.resolve_canonical("red"), Some("nbart_red"));
        assert_eq!(e84.resolve_canonical("red"), Some("band4"));
    }
}
