use crate::dimension::Dimension;
use crate::dtype::Field;
use crate::error::{ArrDbError, Result};

/// Schema for a multi-dimensional array.
///
/// Defines the array's name, its dimensions (axes), and its attributes
/// (the typed values stored at each cell).
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ArraySchema {
    /// Array name (unique within an array registry).
    pub name: String,
    /// Ordered list of dimensions (axes).
    pub dimensions: Vec<Dimension>,
    /// Cell value fields (one buffer per attribute, column-oriented within chunks).
    pub attributes: Vec<Field>,
}

impl ArraySchema {
    /// Creates a new array schema, validating constraints.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - No dimensions are provided
    /// - No attributes are provided
    /// - Duplicate dimension or attribute names
    pub fn new(
        name: impl Into<String>,
        dimensions: Vec<Dimension>,
        attributes: Vec<Field>,
    ) -> Result<Self> {
        if dimensions.is_empty() {
            return Err(ArrDbError::Schema(
                "array must have at least one dimension".into(),
            ));
        }
        if attributes.is_empty() {
            return Err(ArrDbError::Schema(
                "array must have at least one attribute".into(),
            ));
        }

        // Check for duplicate names across dimensions and attributes.
        let mut names = std::collections::HashSet::new();
        for dim in &dimensions {
            if !names.insert(&dim.name) {
                return Err(ArrDbError::Schema(format!(
                    "duplicate member name: '{}'",
                    dim.name
                )));
            }
        }
        for attr in &attributes {
            if !names.insert(&attr.name) {
                return Err(ArrDbError::Schema(format!(
                    "duplicate member name: '{}'",
                    attr.name
                )));
            }
        }

        Ok(Self {
            name: name.into(),
            dimensions,
            attributes,
        })
    }

    /// Number of dimensions.
    pub fn ndim(&self) -> usize {
        self.dimensions.len()
    }

    /// Chunk shape derived from dimensions: `[dim0.chunk_size, dim1.chunk_size, ...]`.
    pub fn chunk_shape(&self) -> Vec<u64> {
        self.dimensions.iter().map(|d| d.chunk_size).collect()
    }

    /// Total number of cells per chunk (product of chunk sizes across all dims).
    pub fn cells_per_chunk(&self) -> u64 {
        self.dimensions.iter().map(|d| d.chunk_size).product()
    }

    /// Total number of chunks in the array (product across all dims).
    pub fn total_chunks(&self) -> u64 {
        self.dimensions.iter().map(|d| d.num_chunks()).product()
    }

    /// Look up a dimension by name.
    pub fn dimension(&self, name: &str) -> Option<&Dimension> {
        self.dimensions.iter().find(|d| d.name == name)
    }

    /// Look up an attribute by name.
    pub fn attribute(&self, name: &str) -> Option<&Field> {
        self.attributes.iter().find(|a| a.name == name)
    }

    /// Look up an attribute index by name.
    pub fn attribute_index(&self, name: &str) -> Option<usize> {
        self.attributes.iter().position(|a| a.name == name)
    }
}

impl std::fmt::Display for ArraySchema {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "ARRAY {} (", self.name)?;
        for (i, dim) in self.dimensions.iter().enumerate() {
            let comma = if i + 1 < self.dimensions.len() || !self.attributes.is_empty() {
                ","
            } else {
                ""
            };
            writeln!(f, "    {dim}{comma}")?;
        }
        for (i, attr) in self.attributes.iter().enumerate() {
            let comma = if i + 1 < self.attributes.len() {
                ","
            } else {
                ""
            };
            writeln!(f, "    {} {}{comma}", attr.name, attr.dtype)?;
        }
        write!(f, ")")
    }
}

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

    #[cfg(feature = "serde")]
    #[test]
    fn schema_serde_roundtrip() {
        let schema = ArraySchema::new(
            "test",
            vec![Dimension::new("x", DType::Float64, 0, 99, 10)],
            vec![Field {
                name: "temp".into(),
                dtype: DType::Float32,
            }],
        )
        .unwrap();
        let json = serde_json::to_string(&schema).unwrap();
        let back: ArraySchema = serde_json::from_str(&json).unwrap();
        assert_eq!(back, schema);
    }

    fn climate_schema() -> ArraySchema {
        ArraySchema::new(
            "climate_data",
            vec![
                Dimension::new("x", DType::Float64, 0, 9999, 256),
                Dimension::new("y", DType::Float64, 0, 9999, 256),
            ],
            vec![
                Field {
                    name: "temp".into(),
                    dtype: DType::Float32,
                },
                Field {
                    name: "humid".into(),
                    dtype: DType::Float32,
                },
            ],
        )
        .unwrap()
    }

    #[test]
    fn schema_basic_properties() {
        let s = climate_schema();
        assert_eq!(s.ndim(), 2);
        assert_eq!(s.chunk_shape(), vec![256, 256]);
        assert_eq!(s.cells_per_chunk(), 256 * 256);
        // ceil(10000/256) = 40 per dim → 40*40 = 1600
        assert_eq!(s.total_chunks(), 40 * 40);
    }

    #[test]
    fn schema_lookups() {
        let s = climate_schema();
        assert!(s.dimension("x").is_some());
        assert!(s.dimension("z").is_none());
        assert_eq!(s.attribute_index("humid"), Some(1));
        assert_eq!(s.attribute_index("missing"), None);
    }

    #[test]
    fn schema_no_dimensions() {
        let r = ArraySchema::new(
            "bad",
            vec![],
            vec![Field {
                name: "v".into(),
                dtype: DType::Float32,
            }],
        );
        assert!(r.is_err());
    }

    #[test]
    fn schema_no_attributes() {
        let r = ArraySchema::new(
            "bad",
            vec![Dimension::new("x", DType::Int32, 0, 10, 5)],
            vec![],
        );
        assert!(r.is_err());
    }

    #[test]
    fn schema_duplicate_names() {
        let r = ArraySchema::new(
            "bad",
            vec![Dimension::new("x", DType::Int32, 0, 10, 5)],
            vec![Field {
                name: "x".into(),
                dtype: DType::Float32,
            }],
        );
        assert!(matches!(r, Err(ArrDbError::Schema(_))));
    }

    #[test]
    fn schema_many_dims_allowed() {
        // arrdb-core imposes no dimension limit; the 8-dim cap is
        // enforced by arrdb-buffer's fixed-size chunk header.
        let dims: Vec<_> = (0..12)
            .map(|i| Dimension::new(format!("d{i}"), DType::Int32, 0, 10, 5))
            .collect();
        let r = ArraySchema::new(
            "wide",
            dims,
            vec![Field {
                name: "v".into(),
                dtype: DType::Float32,
            }],
        );
        assert!(r.is_ok());
        assert_eq!(r.unwrap().ndim(), 12);
    }

    #[test]
    fn schema_display() {
        let s = climate_schema();
        let text = s.to_string();
        assert!(text.contains("ARRAY climate_data"));
        assert!(text.contains("temp FLOAT32"));
    }
}
