//! Python bindings for DYF2 file format

use pyo3::prelude::*;
use pyo3::exceptions::PyIOError;
use pyo3::types::PyDict;

use std::collections::HashMap;
use std::io::Write;

use dyf_core::format as fmt;

/// Python wrapper for DyfFile (DYF2 format).
#[pyclass]
pub struct PyDyfFile {
    pub(crate) inner: fmt::DyfFile,
}

#[pymethods]
impl PyDyfFile {
    /// Open an existing DYF2 file.
    #[staticmethod]
    fn open(path: &str) -> PyResult<Self> {
        let inner = fmt::DyfFile::open(path).map_err(|e| PyIOError::new_err(e.to_string()))?;
        Ok(PyDyfFile { inner })
    }

    /// Append a field layer with raw Arrow IPC batch bytes.
    ///
    /// Args:
    ///     name: Layer name (e.g., "community_id")
    ///     field_name: Field name within the layer
    ///     arrow_type: Arrow type string (e.g., "int32", "float32")
    ///     batch_data: List of bytes objects, one per batch
    fn append_field_layer(
        &mut self,
        name: &str,
        field_name: &str,
        arrow_type: &str,
        batch_data: Vec<Vec<u8>>,
    ) -> PyResult<()> {
        let fields = vec![fmt::FieldDescriptor {
            name: field_name.to_string(),
            arrow_type: arrow_type.to_string(),
        }];
        self.inner
            .append_field_layer(name, fields, &batch_data)
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Append overflow item batches.
    ///
    /// Args:
    ///     batch_data: List of raw Arrow IPC bytes
    ///     num_rows: List of row counts for each batch
    fn append_items(&mut self, batch_data: Vec<Vec<u8>>, num_rows: Vec<u32>) -> PyResult<()> {
        self.inner
            .append_items(&batch_data, &num_rows)
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Update metadata key-value pairs.
    fn update_metadata(&mut self, updates: HashMap<String, String>) -> PyResult<()> {
        let pairs: Vec<(&str, &str)> = updates
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect();
        self.inner
            .update_metadata(&pairs)
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Rewrite the footer after tree mutations.
    fn update_tree(&mut self) -> PyResult<()> {
        self.inner
            .update_tree()
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Compact the file: merge overflow batches into base batches.
    /// Full rewrite — use when fragmentation is high.
    fn compact(&mut self) -> PyResult<()> {
        self.inner
            .compact()
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Read a base batch's raw Arrow IPC bytes.
    fn read_base_batch(&self, batch_idx: usize) -> PyResult<Vec<u8>> {
        self.inner
            .read_base_batch(batch_idx)
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Read an overflow batch's raw Arrow IPC bytes.
    fn read_overflow_batch(&self, batch_idx: usize) -> PyResult<Vec<u8>> {
        self.inner
            .read_overflow_batch(batch_idx)
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Read a field layer batch's raw Arrow IPC bytes.
    fn read_field_layer_batch(&self, layer_idx: usize, batch_idx: usize) -> PyResult<Vec<u8>> {
        self.inner
            .read_field_layer_batch(layer_idx, batch_idx)
            .map_err(|e| PyIOError::new_err(e.to_string()))
    }

    /// Fragmentation ratio (overflow_items / total_items).
    #[getter]
    fn fragmentation(&self) -> f32 {
        self.inner.fragmentation()
    }

    /// Total items in the file.
    #[getter]
    fn total_items(&self) -> u64 {
        self.inner.total_items()
    }

    /// Embedding dimension.
    #[getter]
    fn embedding_dim(&self) -> u16 {
        self.inner.embedding_dim()
    }

    /// Number of base batches.
    #[getter]
    fn num_base_batches(&self) -> usize {
        self.inner.base_batches().len()
    }

    /// Number of overflow batches.
    #[getter]
    fn num_overflow_batches(&self) -> usize {
        self.inner.overflow_batches_desc().len()
    }

    /// Number of field layers.
    #[getter]
    fn num_field_layers(&self) -> usize {
        self.inner.field_layers().len()
    }

    /// Metadata as a Python dict.
    #[getter]
    fn metadata<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
        let dict = PyDict::new(py);
        for (k, v) in self.inner.metadata() {
            dict.set_item(k, v)?;
        }
        Ok(dict)
    }

    /// Field layer names.
    #[getter]
    fn field_layer_names(&self) -> Vec<String> {
        self.inner
            .field_layers()
            .iter()
            .map(|fl| fl.name.clone())
            .collect()
    }

    /// File path.
    #[getter]
    fn path(&self) -> String {
        self.inner.path().to_string_lossy().to_string()
    }

    fn __repr__(&self) -> String {
        format!(
            "DyfFile(path='{}', items={}, dim={}, base_batches={}, overflow={}, layers={})",
            self.inner.path().display(),
            self.inner.total_items(),
            self.inner.embedding_dim(),
            self.inner.base_batches().len(),
            self.inner.overflow_batches_desc().len(),
            self.inner.field_layers().len(),
        )
    }
}
